detached be1cf92

refactor(protocol): extract presentation contracts (#227)

* refactor(protocol): extract presentation contracts

* docs(architecture): describe presentation contract ownership

* test(protocol): check decoded same-peer switch types

* test(protocol): isolate route validation cases
commit
be1cf92256b87ad8a550fa1859f00e291b031861
author
bnema · · 2026-08-28 11:44 UTC
committer
GitHub · · 2026-08-28 11:44 UTC
parent
040f576
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
117 files · +2183 -2059
M AGENTS.md +1 -1
@@ -61,7 +61,7 @@ Before touching daemon teardown paths, read the lock-ordering notes at the top o
6161
6262 ## Wire protocol
6363
64-Wire payload types/codecs live in `internal/ports/frame.go` and `internal/ports/wire.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
64+Semantic presentation and route values live in `internal/protocol`; session-control values live in `internal/ports/session_control.go`. Frame IDs and codecs live in `internal/ports/frame.go`, `internal/ports/wire.go`, `internal/ports/routes_wire.go`, and `internal/ports/wire_command.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
6565
6666 - IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.
6767 - Client message types occupy `1–13`, `15`, `32–33`, and `35` (`MsgParkedRouteRequest`); server types occupy `16–23`, `25–31`, `34`, and `36` (`MsgParkedRouteResponse`). Types `14` and `24` remain reserved.
M internal/adapters/dgram/network_sim_test.go +4 -3
@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/bnema/vev/internal/domain"
1313 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
1415 pdgram "github.com/bnema/vev/pkg/dgram"
1516 )
1617
@@ -452,7 +453,7 @@ func TestTransportFloodClassification(t *testing.T) {
452453 }
453454 }
454455 b.mu.Unlock()
455- primer := mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("primer")})
456+ primer := mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("primer")})
456457 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {
457458 t.Fatal(err)
458459 }
@@ -529,7 +530,7 @@ func TestTransportFloodClassification(t *testing.T) {
529530 }
530531 })
531532 for state := 1; state < outputCount; state++ {
532- payload := mustMarshalOutput(ports.Output{
533+ payload := mustMarshalOutput(protocol.Output{
533534 Epoch: 1,
534535 Base: uint64(state),
535536 New: uint64(state + 1),
@@ -854,7 +855,7 @@ func floodState(sender, receiver *Transport, link *simulatedLink) floodTransport
854855 }
855856
856857 func floodOutputPayload(baseState, mtu int) []byte {
857- return mustMarshalOutput(ports.Output{
858+ return mustMarshalOutput(protocol.Output{
858859 Epoch: 1,
859860 Base: uint64(baseState),
860861 New: uint64(baseState + 1),
M internal/adapters/dgram/proxy_test.go +2 -1
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 )
1617
1718 type fakeTransport struct {
@@ -105,7 +106,7 @@ func TestProxyRuntimeClampsDatagramHelloOutputWindowAndPreservesControl(t *testi
105106 }
106107
107108 input := ports.Frame{Type: ports.MsgInput, Payload: []byte("still flowing")}
108- ack := ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: 1, State: 7})}
109+ ack := ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: 1, State: 7})}
109110 client.recv <- recvResult{frame: input}
110111 client.recv <- recvResult{frame: ack}
111112 for _, want := range []ports.Frame{input, ack} {
M internal/adapters/dgram/transport_send_test.go +4 -3
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 pdgram "github.com/bnema/vev/pkg/dgram"
1314 "github.com/stretchr/testify/require"
1415 )
@@ -114,7 +115,7 @@ func TestOutputPacingSendsAtMostOneOversizedFramePerTick(t *testing.T) {
114115 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("prime")}); err != nil {
115116 t.Fatal(err)
116117 }
117- large := mustMarshalOutput(ports.Output{Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 24*a.mtu)})
118+ large := mustMarshalOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 24*a.mtu)})
118119 for range 2 {
119120 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: large}); err != nil {
120121 t.Fatal(err)
@@ -222,7 +223,7 @@ func TestOwnedSynchronousMaxSideEffectCompletesThroughConcurrentPacedWork(t *tes
222223 }
223224 a.mu.Unlock()
224225 go a.resendPending()
225- sideEffect := ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 100*1024)})}
226+ sideEffect := ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 100*1024)})}
226227 done := make(chan error, 1)
227228 go func() { done <- a.SendSynchronous(sideEffect) }()
228229
@@ -330,7 +331,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
330331 frame ports.Frame
331332 }{
332333 {name: "control", frame: ports.Frame{Type: ports.MsgPing}},
333- {name: "terminal side effect", frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("osc")})}},
334+ {name: "terminal side effect", frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("osc")})}},
334335 }
335336 for _, tt := range frames {
336337 t.Run(tt.name, func(t *testing.T) {
M internal/adapters/dgram/wire_test_helpers_test.go +6 -3
@@ -1,8 +1,11 @@
11 package dgram
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
47
5-func mustMarshalOutput(m ports.Output) []byte {
8+func mustMarshalOutput(m protocol.Output) []byte {
69 payload, err := ports.MarshalOutput(m)
710 if err != nil {
811 panic(err)
@@ -10,7 +13,7 @@ func mustMarshalOutput(m ports.Output) []byte {
1013 return payload
1114 }
1215
13-func mustMarshalAck(m ports.Ack) []byte {
16+func mustMarshalAck(m protocol.Ack) []byte {
1417 payload, err := ports.MarshalAck(m)
1518 if err != nil {
1619 panic(err)
M internal/adapters/ipc/transport_test.go +2 -1
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
1617 )
1718
1819 func TestTransportSendRecvBothDirections(t *testing.T) {
@@ -140,7 +141,7 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
140141 }
141142 // Vary a couple to prove ordering and content are both preserved.
142143 frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}
143- frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(ports.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
144+ frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
144145
145146 var wg sync.WaitGroup
146147 wg.Go(func() {
M internal/adapters/ipc/wire_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
11 package ipc
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
47
5-func mustMarshalResize(m ports.Resize) []byte {
8+func mustMarshalResize(m protocol.Resize) []byte {
69 payload, err := ports.MarshalResize(m)
710 if err != nil {
811 panic(err)
M internal/adapters/remote/preview.go +15 -14
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/vev/internal/adapters/sshstdio"
1212 "github.com/bnema/vev/internal/domain"
1313 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
1415 )
1516
1617 const (
@@ -39,11 +40,11 @@ func NewPreviewClient() ports.RemotePreviewClient {
3940 return &PreviewClient{command: exec.CommandContext}
4041 }
4142
42-func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
43- request := ports.RemotePreviewRequest{Version: ports.RemotePreviewSchemaVersion, Target: target, Width: width, Height: height}
43+func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
44+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: width, Height: height}
4445 payload := ports.MarshalRemotePreviewRequest(request)
4546 if payload == nil {
46- return ports.RemotePreview{}, ports.ErrInvalidRemotePreviewRequest
47+ return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreviewRequest
4748 }
4849 runCtx, cancel := context.WithTimeout(ctx, c.previewTimeout())
4950 defer cancel()
@@ -54,7 +55,7 @@ func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSession
5455 encoded := base64.RawURLEncoding.EncodeToString(payload)
5556 spec := sshstdio.BuildCommandForRemoteCommand(target.Endpoint, "vev", "_remote-preview", encoded)
5657 cmd := command(runCtx, spec.Path, spec.Args...)
57- stdout := boundedBuffer{limit: ports.RemotePreviewMaxBytes}
58+ stdout := boundedBuffer{limit: protocol.RemotePreviewMaxBytes}
5859 stderr := boundedBuffer{limit: remotePreviewMaxDiagnostic}
5960 cmd.Stdout = &stdout
6061 cmd.Stderr = &stderr
@@ -62,30 +63,30 @@ func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSession
6263 if err := cmd.Run(); err != nil {
6364 if ctxErr := runCtx.Err(); ctxErr != nil {
6465 if parentErr := ctx.Err(); parentErr != nil {
65- return ports.RemotePreview{}, parentErr
66+ return protocol.RemotePreview{}, parentErr
6667 }
67- return ports.RemotePreview{}, ports.ErrRemotePreviewTimeout
68+ return protocol.RemotePreview{}, protocol.ErrRemotePreviewTimeout
6869 }
6970 if stdout.overflow || stderr.overflow {
70- return ports.RemotePreview{}, errRemotePreviewTooLarge
71+ return protocol.RemotePreview{}, errRemotePreviewTooLarge
7172 }
7273 if diagnostic := sanitizeCatalogDiagnostic(string(stderr.Bytes())); diagnostic != "" {
73- return ports.RemotePreview{}, fmt.Errorf("%w: %v: %s", errRemotePreviewSSH, err, diagnostic)
74+ return protocol.RemotePreview{}, fmt.Errorf("%w: %v: %s", errRemotePreviewSSH, err, diagnostic)
7475 }
75- return ports.RemotePreview{}, fmt.Errorf("%w: %v", errRemotePreviewSSH, err)
76+ return protocol.RemotePreview{}, fmt.Errorf("%w: %v", errRemotePreviewSSH, err)
7677 }
7778 if stdout.overflow || stderr.overflow {
78- return ports.RemotePreview{}, errRemotePreviewTooLarge
79+ return protocol.RemotePreview{}, errRemotePreviewTooLarge
7980 }
8081 preview, err := ports.UnmarshalRemotePreview(stdout.Bytes())
8182 if err != nil {
82- return ports.RemotePreview{}, err
83+ return protocol.RemotePreview{}, err
8384 }
84- if preview.Status != ports.RemotePreviewOK {
85- return ports.RemotePreview{}, fmt.Errorf("remote preview unavailable: status %d", preview.Status)
85+ if preview.Status != protocol.RemotePreviewOK {
86+ return protocol.RemotePreview{}, fmt.Errorf("remote preview unavailable: status %d", preview.Status)
8687 }
8788 if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
88- return ports.RemotePreview{}, errRemotePreviewIdentity
89+ return protocol.RemotePreview{}, errRemotePreviewIdentity
8990 }
9091 return preview, nil
9192 }
M internal/adapters/remote/preview_test.go +10 -9
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev/internal/adapters/sshstdio"
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
1920 )
2021
2122 func previewClientTargetForTest() domain.RemoteSessionTarget {
@@ -31,8 +32,8 @@ func TestRemotePreviewClientBuildsExactSSHCommandAndDecodesResponse(t *testing.T
3132 Endpoint: "build@mule", DisplayOrigin: "mule", LifecycleID: lifecycle,
3233 SessionName: "work", LiveTabID: "tab-1",
3334 }
34- want := ports.RemotePreview{
35- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,
35+ want := protocol.RemotePreview{
36+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
3637 LifecycleID: lifecycle, TabID: target.LiveTabID, Revision: 7, Width: 1, Height: 1,
3738 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
3839 }
@@ -79,17 +80,17 @@ func TestRemotePreviewClientRejectsMismatchedResponseIdentity(t *testing.T) {
7980 target := previewClientTargetForTest()
8081 var otherLifecycle domain.SessionLifecycleID
8182 otherLifecycle[0] = 2
82- valid := ports.RemotePreview{
83- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,
83+ valid := protocol.RemotePreview{
84+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
8485 LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1, Width: 1, Height: 1,
8586 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
8687 }
8788 tests := []struct {
8889 name string
89- mutate func(*ports.RemotePreview)
90+ mutate func(*protocol.RemotePreview)
9091 }{
91- {name: "lifecycle ID", mutate: func(preview *ports.RemotePreview) { preview.LifecycleID = otherLifecycle }},
92- {name: "tab ID", mutate: func(preview *ports.RemotePreview) { preview.TabID = "tab-2" }},
92+ {name: "lifecycle ID", mutate: func(preview *protocol.RemotePreview) { preview.LifecycleID = otherLifecycle }},
93+ {name: "tab ID", mutate: func(preview *protocol.RemotePreview) { preview.TabID = "tab-2" }},
9394 }
9495 for _, tt := range tests {
9596 t.Run(tt.name, func(t *testing.T) {
@@ -120,7 +121,7 @@ func TestRemotePreviewClientRejectsMalformedResponse(t *testing.T) {
120121
121122 func TestRemotePreviewClientRejectsOversizedOutput(t *testing.T) {
122123 client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
123- return exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("head -c %d /dev/zero", ports.RemotePreviewMaxBytes+1))
124+ return exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("head -c %d /dev/zero", protocol.RemotePreviewMaxBytes+1))
124125 }}
125126 target := previewClientTargetForTest()
126127 _, err := client.Preview(context.Background(), target, 1, 1)
@@ -145,7 +146,7 @@ func TestRemotePreviewClientReturnsInternalTimeoutSentinel(t *testing.T) {
145146 },
146147 }
147148 _, err := client.Preview(context.Background(), previewClientTargetForTest(), 1, 1)
148- require.ErrorIs(t, err, ports.ErrRemotePreviewTimeout)
149+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTimeout)
149150 }
150151
151152 func TestRemotePreviewClientReturnsCallerCancellation(t *testing.T) {
M internal/app/final_acceptance_test.go +6 -5
@@ -8,6 +8,7 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 "github.com/bnema/vev/internal/usecase/daemon"
1213 "github.com/stretchr/testify/require"
1314 )
@@ -46,7 +47,7 @@ func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.Comma
4647 return result
4748 }
4849
49-func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output {
50+func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(protocol.Output) bool) protocol.Output {
5051 t.Helper()
5152 frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool {
5253 output, err := ports.UnmarshalOutput(frame.Payload)
@@ -113,15 +114,15 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
113114 // The resize keeps its attachment-local output stream while recalculating
114115 // shared PTY/content geometry from the latest valid attachment claim; the
115116 // peer stream remains independent.
116- resizePayload, err := ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}})
117+ resizePayload, err := ports.MarshalResize(protocol.Resize{Size: domain.Size{Cols: 100, Rows: 30}})
117118 require.NoError(t, err)
118119 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload}))
119- resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) })
120+ resized := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) })
120121 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)
121122 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
122123
123124 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
124- reset := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Base == 0 && output.Full })
125+ reset := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Base == 0 && output.Full })
125126 require.True(t, reset.Full)
126127 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
127128
@@ -173,6 +174,6 @@ func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
173174
174175 drained:
175176 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
176- awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Full && output.Base == 0 })
177+ awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Full && output.Base == 0 })
177178 assertNoTextAfterInput(t, secondPump, size, "Commands")
178179 }
M internal/app/remote_final_acceptance_test.go +3 -2
@@ -15,6 +15,7 @@ import (
1515 "github.com/bnema/vev/internal/domain"
1616 "github.com/bnema/vev/internal/ports"
1717 portsmocks "github.com/bnema/vev/internal/ports/mocks"
18+ "github.com/bnema/vev/internal/protocol"
1819 "github.com/bnema/vev/internal/usecase/client"
1920 "github.com/stretchr/testify/require"
2021 )
@@ -78,7 +79,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
7879 if outputs != nil {
7980 outputs <- string(data)
8081 }
81- payload, err := ports.MarshalOutput(ports.Output{
82+ payload, err := ports.MarshalOutput(protocol.Output{
8283 Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data,
8384 })
8485 if err != nil {
@@ -155,7 +156,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
155156 welcome, err := wireTransport.Recv()
156157 require.NoError(t, err)
157158 require.Equal(t, ports.MsgWelcome, welcome.Type)
158- require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{})}))
159+ require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{})}))
159160 attachTarget, err := wireTransport.Recv()
160161 require.NoError(t, err)
161162 target, err := ports.UnmarshalAttachTarget(attachTarget.Payload)
M internal/app/remote_preview.go +4 -3
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/adapters/ipc"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 // runRemotePreview is the hidden SSH-side carriage. The request is encoded in
@@ -27,7 +28,7 @@ func runRemotePreview(ctx context.Context, encoded string) error {
2728 if ctxErr := ctx.Err(); ctxErr != nil {
2829 return ctxErr
2930 }
30- return writeRemotePreviewStatus(ports.RemotePreviewUnavailable)
31+ return writeRemotePreviewStatus(protocol.RemotePreviewUnavailable)
3132 }
3233 defer func() { _ = transport.Close() }()
3334 if err := transport.Send(ports.Frame{Type: ports.MsgRemotePreviewRequest, Payload: request}); err != nil {
@@ -63,8 +64,8 @@ func runRemotePreview(ctx context.Context, encoded string) error {
6364 }
6465 }
6566
66-func writeRemotePreviewStatus(status ports.RemotePreviewStatus) error {
67- payload := ports.MarshalRemotePreview(ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: status})
67+func writeRemotePreviewStatus(status protocol.RemotePreviewStatus) error {
68+ payload := ports.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: status})
6869 if payload == nil {
6970 return errors.New("vev: failed to encode remote preview status")
7071 }
M internal/app/run.go +8 -7
@@ -46,6 +46,7 @@ import (
4646 "github.com/bnema/vev/internal/persist"
4747 "github.com/bnema/vev/internal/platform"
4848 "github.com/bnema/vev/internal/ports"
49+ "github.com/bnema/vev/internal/protocol"
4950 "github.com/bnema/vev/internal/usecase/client"
5051 "github.com/bnema/vev/internal/usecase/daemon"
5152 "github.com/bnema/vev/internal/usecase/recovery"
@@ -892,10 +893,10 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
892893 var remoteSelection *domain.RemoteSessionTarget
893894 var remoteEnvironmentPolicy ports.EnvironmentPolicy
894895 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)
895- routeOrigin := ports.RouteOriginLocal
896+ routeOrigin := protocol.RouteOriginLocal
896897 routeOriginKey := "local"
897898 if remoteTarget != "" {
898- routeOrigin = ports.RouteOriginRemote
899+ routeOrigin = protocol.RouteOriginRemote
899900 routeOriginKey = remoteTarget
900901 if modeErr != nil {
901902 return modeErr
@@ -939,7 +940,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
939940 Intent: target.Intent,
940941 SessionName: target.Session,
941942 Remote: true,
942- Origin: ports.RouteOriginDiscovery,
943+ Origin: protocol.RouteOriginDiscovery,
943944 OriginKey: target.Endpoint,
944945 RemoteTarget: selection,
945946 HostLabel: displayOrigin,
@@ -968,7 +969,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
968969 RemoteHostLearner: attachRememberLearner(deps, remoteTarget, log),
969970 AttachHandoff: handoff,
970971 Remote: true,
971- Origin: ports.RouteOriginRemote,
972+ Origin: protocol.RouteOriginRemote,
972973 OriginKey: remoteTarget,
973974 }, client.AttachRequest{
974975 Intent: intent,
@@ -992,9 +993,9 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
992993 RuntimeObserver: deps.runtimeObserver,
993994 AttachHandoff: handoff,
994995 Remote: false,
995- Origin: ports.RouteOriginLocal,
996+ Origin: protocol.RouteOriginLocal,
996997 OriginKey: "local",
997- }, client.AttachRequest{Intent: intent, SessionName: name, Origin: ports.RouteOriginLocal, OriginKey: "local"})
998+ }, client.AttachRequest{Intent: intent, SessionName: name, Origin: protocol.RouteOriginLocal, OriginKey: "local"})
998999 }
9991000
10001001 var handoffErr *client.AttachTargetError
@@ -1013,7 +1014,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
10131014 handoffAttempts++
10141015 remoteTarget = handoffErr.Target.Endpoint
10151016 remoteDisplayOrigin = domain.RemoteDisplayOrigin(remoteTarget)
1016- routeOrigin = ports.RouteOriginDiscovery
1017+ routeOrigin = protocol.RouteOriginDiscovery
10171018 routeOriginKey = remoteTarget
10181019 name = handoffErr.Target.Session
10191020 intent = handoffErr.Target.Intent
M internal/app/run_test.go +3 -2
@@ -22,6 +22,7 @@ import (
2222 "github.com/bnema/vev/internal/persist"
2323 "github.com/bnema/vev/internal/ports"
2424 portsmocks "github.com/bnema/vev/internal/ports/mocks"
25+ "github.com/bnema/vev/internal/protocol"
2526 "github.com/bnema/vev/internal/usecase/client"
2627 "github.com/bnema/vev/internal/usecase/daemon"
2728 "github.com/bnema/vev/pkg/kv"
@@ -962,11 +963,11 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
962963 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
963964 if deps.Remote {
964965 remoteCalls++
965- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "picked", Remote: true, Origin: ports.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, request)
966+ require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "picked", Remote: true, Origin: protocol.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, request)
966967 return nil
967968 }
968969 localCalls++
969- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: ports.RouteOriginLocal, OriginKey: "local"}, request)
970+ require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)
970971 return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}
971972 },
972973 })
M internal/ports/frame.go +11 -147
@@ -1,27 +1,9 @@
11 package ports
22
3-import (
4- "time"
5-
6- renderer "github.com/bnema/vev-vt"
7- "github.com/bnema/vev/internal/domain"
8-)
9-
10-// HandshakeTimeout bounds every transport handshake from connect through the
11-// first committed publication.
12-const HandshakeTimeout = 15 * time.Second
13-
14-// ProtocolVersion is the current vev IPC wire protocol version.
15-const ProtocolVersion uint16 = 37
16-
173 // MaxFrameLen is the largest permitted frame length, including the type byte
184 // and excluding the four-byte length prefix.
195 const MaxFrameLen = 16 << 20
206
21-// MaxOutputDataLen is the largest decoded terminal byte stream that fits in
22-// one Output frame after its type byte and wire metadata.
23-const MaxOutputDataLen = MaxFrameLen - 1 - outputPayloadOverhead
24-
257 // MsgType identifies the kind of payload carried by a Frame.
268 type MsgType uint8
279
@@ -48,18 +30,16 @@ const (
4830 MsgSamePeerSwitchRequest MsgType = 33
4931 MsgParkedRouteRequest MsgType = 35
5032
51- MsgWelcome MsgType = 16
52- MsgError MsgType = 17
53- MsgOutput MsgType = 18
54- MsgDetached MsgType = 19
55- MsgPong MsgType = 20
56- MsgSessions MsgType = 21
57- MsgCommandResult MsgType = 22
58- MsgNavigationAction MsgType = 23
59- MsgAttachTarget MsgType = 25
60- MsgRemotePreviewResponse MsgType = 26
61- // Route navigation metadata/control frames occupy the post-26 server range;
62- // 14 and 24 remain reserved for compatibility with older peers.
33+ MsgWelcome MsgType = 16
34+ MsgError MsgType = 17
35+ MsgOutput MsgType = 18
36+ MsgDetached MsgType = 19
37+ MsgPong MsgType = 20
38+ MsgSessions MsgType = 21
39+ MsgCommandResult MsgType = 22
40+ MsgNavigationAction MsgType = 23
41+ MsgAttachTarget MsgType = 25
42+ MsgRemotePreviewResponse MsgType = 26
6343 MsgCommittedRouteIdentity MsgType = 27
6444 MsgRecentRouteSnapshot MsgType = 28
6545 MsgNavigateRecentRoute MsgType = 29
@@ -70,124 +50,8 @@ const (
7050 )
7151
7252 // Frame is the unit of exchange over a Transport: a typed, length-delimited
73-// message. It lives in ports (rather than an ipc adapter) so usecases and
74-// adapters can share it without either depending on a concrete transport
75-// implementation.
53+// message.
7654 type Frame struct {
7755 Type MsgType
7856 Payload []byte
7957 }
80-
81-// NavigationCapabilities advertises the bounded client routes available to a
82-// directly attached daemon.
83-type NavigationCapabilities uint8
84-
85-const (
86- NavigationCapabilityHomePicker NavigationCapabilities = 1 << iota
87- NavigationCapabilityBack
88-)
89-
90-// StartupOverlay selects the one startup overlay opened by a navigation route.
91-type StartupOverlay uint8
92-
93-const (
94- StartupOverlayNone StartupOverlay = iota
95- StartupOverlaySessionPicker
96-)
97-
98-// NavigationAction is a server request for one bounded client route transition.
99-type NavigationAction uint8
100-
101-const (
102- NavigationOpenHomePicker NavigationAction = 1
103- NavigationBack NavigationAction = 2
104-)
105-
106-// ParkedRouteLeaseID is an opaque capability minted by the attached daemon for
107-// one home-picker excursion. It is bound server-side to the exact connection
108-// generation and transport that received the navigation directive.
109-type ParkedRouteLeaseID [16]byte
110-
111-func (id ParkedRouteLeaseID) IsZero() bool { return id == ParkedRouteLeaseID{} }
112-
113-// NavigationDirective requests one client-owned route transition. Opening the
114-// home picker always carries a fresh lease; Back is sent by the transient home
115-// route and therefore carries no lease.
116-type NavigationDirective struct {
117- Action NavigationAction
118- LeaseID ParkedRouteLeaseID
119-}
120-
121-// ParkedRouteAction advances one retained route through its one-shot lease.
122-type ParkedRouteAction uint8
123-
124-const (
125- ParkedRoutePrepare ParkedRouteAction = iota + 1
126- ParkedRouteResume
127- ParkedRouteSwitch
128-)
129-
130-// ParkedRouteRequest prepares, resumes, or switches the retained authenticated
131-// route. Target is present only for Switch and retains exact lifecycle/tab
132-// authority from the home picker's remote catalogue row.
133-type ParkedRouteRequest struct {
134- RequestID uint64
135- LeaseID ParkedRouteLeaseID
136- Action ParkedRouteAction
137- Target *domain.RemoteSessionTarget
138-}
139-
140-// ParkedRouteStatus is the explicit outcome for a retained-route request.
141-type ParkedRouteStatus uint8
142-
143-const (
144- ParkedRouteReady ParkedRouteStatus = iota + 1
145- ParkedRouteResumed
146- ParkedRouteSwitched
147- ParkedRouteRejected
148- ParkedRouteExpired
149- ParkedRouteStaleTarget
150-)
151-
152-// ParkedRouteResponse correlates one retained-route request with its outcome.
153-type ParkedRouteResponse struct {
154- RequestID uint64
155- Status ParkedRouteStatus
156-}
157-
158-// RemotePreviewSchemaVersion is independent from the attachment IPC version.
159-const RemotePreviewSchemaVersion uint16 = 1
160-
161-// RemotePreviewStatus is a closed response taxonomy. Terminal contents are
162-// never carried in an error response.
163-type RemotePreviewStatus uint8
164-
165-const (
166- RemotePreviewOK RemotePreviewStatus = iota
167- RemotePreviewUnavailable
168- RemotePreviewNoSuchTarget
169- RemotePreviewStale
170- RemotePreviewMalformed
171- RemotePreviewTooLarge
172-)
173-
174-// RemotePreviewRequest asks the owning daemon for an in-memory live viewport.
175-type RemotePreviewRequest struct {
176- Version uint16
177- Target domain.RemoteSessionTarget
178- Width uint16
179- Height uint16
180-}
181-
182-// RemotePreview is a bounded row-major styled-cell viewport. It is a
183-// process-memory DTO and is never persisted, logged, or traced.
184-type RemotePreview struct {
185- Version uint16
186- Status RemotePreviewStatus
187- LifecycleID domain.SessionLifecycleID
188- TabID domain.TabStableID
189- Revision uint64
190- Width uint16
191- Height uint16
192- Cells []renderer.Cell
193-}
M internal/ports/frame_test.go +3 -1
@@ -1,6 +1,8 @@
11 package ports
22
3-import "testing"
3+import (
4+ "testing"
5+)
46
57 func TestProtocolVersion(t *testing.T) {
68 if ProtocolVersion != 37 {
M internal/ports/mocks/remote_mock.go +8 -7
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 mock "github.com/stretchr/testify/mock"
1415 )
1516
@@ -666,22 +667,22 @@ func (_m *MockRemotePreviewClient) EXPECT() *MockRemotePreviewClient_Expecter {
666667 }
667668
668669 // Preview provides a mock function for the type MockRemotePreviewClient
669-func (_mock *MockRemotePreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (ports.RemotePreview, error) {
670+func (_mock *MockRemotePreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (protocol.RemotePreview, error) {
670671 ret := _mock.Called(ctx, target, width, height)
671672
672673 if len(ret) == 0 {
673674 panic("no return value specified for Preview")
674675 }
675676
676- var r0 ports.RemotePreview
677+ var r0 protocol.RemotePreview
677678 var r1 error
678- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) (ports.RemotePreview, error)); ok {
679+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) (protocol.RemotePreview, error)); ok {
679680 return returnFunc(ctx, target, width, height)
680681 }
681- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) ports.RemotePreview); ok {
682+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) protocol.RemotePreview); ok {
682683 r0 = returnFunc(ctx, target, width, height)
683684 } else {
684- r0 = ret.Get(0).(ports.RemotePreview)
685+ r0 = ret.Get(0).(protocol.RemotePreview)
685686 }
686687 if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) error); ok {
687688 r1 = returnFunc(ctx, target, width, height)
@@ -733,12 +734,12 @@ func (_c *MockRemotePreviewClient_Preview_Call) Run(run func(ctx context.Context
733734 return _c
734735 }
735736
736-func (_c *MockRemotePreviewClient_Preview_Call) Return(remotePreview ports.RemotePreview, err error) *MockRemotePreviewClient_Preview_Call {
737+func (_c *MockRemotePreviewClient_Preview_Call) Return(remotePreview protocol.RemotePreview, err error) *MockRemotePreviewClient_Preview_Call {
737738 _c.Call.Return(remotePreview, err)
738739 return _c
739740 }
740741
741-func (_c *MockRemotePreviewClient_Preview_Call) RunAndReturn(run func(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (ports.RemotePreview, error)) *MockRemotePreviewClient_Preview_Call {
742+func (_c *MockRemotePreviewClient_Preview_Call) RunAndReturn(run func(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (protocol.RemotePreview, error)) *MockRemotePreviewClient_Preview_Call {
742743 _c.Call.Return(run)
743744 return _c
744745 }
M internal/ports/navigation_test.go +45 -44
@@ -5,22 +5,23 @@ import (
55 "testing"
66
77 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
89 "github.com/stretchr/testify/require"
910 )
1011
1112 func TestNavigationDirectiveWireTable(t *testing.T) {
12- lease := ParkedRouteLeaseID{1, 2, 3}
13+ lease := protocol.ParkedRouteLeaseID{1, 2, 3}
1314 tests := []struct {
1415 name string
15- directive NavigationDirective
16+ directive protocol.NavigationDirective
1617 valid bool
1718 }{
18- {name: "open home", directive: NavigationDirective{Action: NavigationOpenHomePicker, LeaseID: lease}, valid: true},
19- {name: "back", directive: NavigationDirective{Action: NavigationBack}, valid: true},
20- {name: "open home without lease", directive: NavigationDirective{Action: NavigationOpenHomePicker}},
21- {name: "back with lease", directive: NavigationDirective{Action: NavigationBack, LeaseID: lease}},
22- {name: "zero", directive: NavigationDirective{}},
23- {name: "unknown", directive: NavigationDirective{Action: 3}},
19+ {name: "open home", directive: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}, valid: true},
20+ {name: "back", directive: protocol.NavigationDirective{Action: protocol.NavigationBack}, valid: true},
21+ {name: "open home without lease", directive: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker}},
22+ {name: "back with lease", directive: protocol.NavigationDirective{Action: protocol.NavigationBack, LeaseID: lease}},
23+ {name: "zero", directive: protocol.NavigationDirective{}},
24+ {name: "unknown", directive: protocol.NavigationDirective{Action: 3}},
2425 }
2526
2627 for _, tt := range tests {
@@ -35,25 +36,25 @@ func TestNavigationDirectiveWireTable(t *testing.T) {
3536 require.Equal(t, tt.directive, decoded)
3637 assertAllPrefixesFail(t, got, UnmarshalNavigationDirective)
3738 _, err = UnmarshalNavigationDirective(append(append([]byte(nil), got...), 0))
38- require.ErrorIs(t, err, ErrInvalidNavigation)
39+ require.ErrorIs(t, err, protocol.ErrInvalidNavigation)
3940 })
4041 }
4142 }
4243
4344 func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
44- lease := ParkedRouteLeaseID{1, 2, 3}
45+ lease := protocol.ParkedRouteLeaseID{1, 2, 3}
4546 target := &domain.RemoteSessionTarget{
4647 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{4},
4748 SessionName: "work", LiveTabID: "tab-1",
4849 }
4950 requests := []struct {
5051 name string
51- value ParkedRouteRequest
52+ value protocol.ParkedRouteRequest
5253 golden string
5354 }{
54- {name: "prepare", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRoutePrepare}, golden: "0000000000000001" + "01020300000000000000000000000000" + "010001"},
55- {name: "resume", value: ParkedRouteRequest{RequestID: 2, LeaseID: lease, Action: ParkedRouteResume}},
56- {name: "switch", value: ParkedRouteRequest{RequestID: 3, LeaseID: lease, Action: ParkedRouteSwitch, Target: target}},
55+ {name: "prepare", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare}, golden: "0000000000000001" + "01020300000000000000000000000000" + "010001"},
56+ {name: "resume", value: protocol.ParkedRouteRequest{RequestID: 2, LeaseID: lease, Action: protocol.ParkedRouteResume}},
57+ {name: "switch", value: protocol.ParkedRouteRequest{RequestID: 3, LeaseID: lease, Action: protocol.ParkedRouteSwitch, Target: target}},
5758 }
5859 for _, tt := range requests {
5960 t.Run("request "+tt.name, func(t *testing.T) {
@@ -74,15 +75,15 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
7475
7576 responses := []struct {
7677 name string
77- value ParkedRouteResponse
78+ value protocol.ParkedRouteResponse
7879 golden string
7980 }{
80- {name: "ready", value: ParkedRouteResponse{RequestID: 1, Status: ParkedRouteReady}, golden: "000000000000000101"},
81- {name: "resumed", value: ParkedRouteResponse{RequestID: 2, Status: ParkedRouteResumed}},
82- {name: "switched", value: ParkedRouteResponse{RequestID: 3, Status: ParkedRouteSwitched}},
83- {name: "rejected", value: ParkedRouteResponse{RequestID: 4, Status: ParkedRouteRejected}},
84- {name: "expired", value: ParkedRouteResponse{RequestID: 5, Status: ParkedRouteExpired}},
85- {name: "stale target", value: ParkedRouteResponse{RequestID: 6, Status: ParkedRouteStaleTarget}},
81+ {name: "ready", value: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}, golden: "000000000000000101"},
82+ {name: "resumed", value: protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed}},
83+ {name: "switched", value: protocol.ParkedRouteResponse{RequestID: 3, Status: protocol.ParkedRouteSwitched}},
84+ {name: "rejected", value: protocol.ParkedRouteResponse{RequestID: 4, Status: protocol.ParkedRouteRejected}},
85+ {name: "expired", value: protocol.ParkedRouteResponse{RequestID: 5, Status: protocol.ParkedRouteExpired}},
86+ {name: "stale target", value: protocol.ParkedRouteResponse{RequestID: 6, Status: protocol.ParkedRouteStaleTarget}},
8687 }
8788 for _, tt := range responses {
8889 t.Run("response "+tt.name, func(t *testing.T) {
@@ -102,15 +103,15 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
102103
103104 invalidRequests := []struct {
104105 name string
105- value ParkedRouteRequest
106+ value protocol.ParkedRouteRequest
106107 }{
107- {name: "zero request ID", value: ParkedRouteRequest{LeaseID: lease, Action: ParkedRoutePrepare}},
108- {name: "zero lease", value: ParkedRouteRequest{RequestID: 1, Action: ParkedRoutePrepare}},
109- {name: "unknown action", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: 99}},
110- {name: "prepare target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRoutePrepare, Target: target}},
111- {name: "resume target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteResume, Target: target}},
112- {name: "missing switch target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteSwitch}},
113- {name: "invalid switch target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}},
108+ {name: "zero request ID", value: protocol.ParkedRouteRequest{LeaseID: lease, Action: protocol.ParkedRoutePrepare}},
109+ {name: "zero lease", value: protocol.ParkedRouteRequest{RequestID: 1, Action: protocol.ParkedRoutePrepare}},
110+ {name: "unknown action", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: 99}},
111+ {name: "prepare target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare, Target: target}},
112+ {name: "resume target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteResume, Target: target}},
113+ {name: "missing switch target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteSwitch}},
114+ {name: "invalid switch target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}},
114115 }
115116 for _, tt := range invalidRequests {
116117 t.Run("invalid request "+tt.name, func(t *testing.T) {
@@ -119,11 +120,11 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
119120 }
120121 for _, tt := range []struct {
121122 name string
122- value ParkedRouteResponse
123+ value protocol.ParkedRouteResponse
123124 }{
124- {name: "zero request ID", value: ParkedRouteResponse{Status: ParkedRouteReady}},
125- {name: "zero status", value: ParkedRouteResponse{RequestID: 1}},
126- {name: "unknown status", value: ParkedRouteResponse{RequestID: 1, Status: 99}},
125+ {name: "zero request ID", value: protocol.ParkedRouteResponse{Status: protocol.ParkedRouteReady}},
126+ {name: "zero status", value: protocol.ParkedRouteResponse{RequestID: 1}},
127+ {name: "unknown status", value: protocol.ParkedRouteResponse{RequestID: 1, Status: 99}},
127128 } {
128129 t.Run("invalid response "+tt.name, func(t *testing.T) {
129130 require.Nil(t, MarshalParkedRouteResponse(tt.value))
@@ -141,22 +142,22 @@ func TestHelloNavigationValidationTable(t *testing.T) {
141142 name string
142143 hello Hello
143144 valid bool
144- capability NavigationCapabilities
145- overlay StartupOverlay
145+ capability protocol.NavigationCapabilities
146+ overlay protocol.StartupOverlay
146147 wantPayload string
147148 }{
148149 {name: "ordinary", hello: base, valid: true},
149150 {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},
150- {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker},
151- {name: "remote-target home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker},
152- {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false},
153- {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
154- {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
155- {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},
156- {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false},
151+ {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
152+ {name: "remote-target home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
153+ {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
154+ {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
155+ {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
156+ {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: true, capability: protocol.NavigationCapabilityBack, overlay: protocol.StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},
157+ {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
157158 {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false},
158- {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false},
159- {name: "picker without back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: StartupOverlaySessionPicker}, valid: false},
159+ {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
160+ {name: "picker without back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
160161 }
161162
162163 for _, tt := range tests {
M internal/ports/output_compression_test.go +6 -5
@@ -6,10 +6,11 @@ import (
66 "testing"
77
88 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 func TestOutputCompression(t *testing.T) {
12- full := Output{
13+ full := protocol.Output{
1314 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
1415 Data: bytes.Repeat([]byte("\x1b[38;2;120;180;240mstyled viewport row\x1b[0m\r\n"), 128),
1516 }
@@ -20,12 +21,12 @@ func TestOutputCompression(t *testing.T) {
2021
2122 for _, tt := range []struct {
2223 name string
23- output Output
24+ output protocol.Output
2425 wantKind byte
2526 }{
2627 {name: "large full snapshot", output: full, wantKind: outputCompressionZlib},
2728 {name: "incremental output", output: incremental, wantKind: outputCompressionNone},
28- {name: "small full snapshot", output: Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("small")}, wantKind: outputCompressionNone},
29+ {name: "small full snapshot", output: protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("small")}, wantKind: outputCompressionNone},
2930 } {
3031 t.Run(tt.name, func(t *testing.T) {
3132 payload, err := MarshalOutput(tt.output)
@@ -47,7 +48,7 @@ func TestOutputCompression(t *testing.T) {
4748 }
4849
4950 func TestCompressedOutputRejectsMalformedPayloads(t *testing.T) {
50- output := Output{
51+ output := protocol.Output{
5152 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
5253 Data: bytes.Repeat([]byte("\x1b[31mcompressed\x1b[0m\r\n"), 256),
5354 }
@@ -92,7 +93,7 @@ func TestCompressedOutputRejectsMalformedPayloads(t *testing.T) {
9293 }
9394
9495 func BenchmarkMarshalOutput(b *testing.B) {
95- fixtures := map[string]Output{
96+ fixtures := map[string]protocol.Output{
9697 "empty-120x40": {
9798 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
9899 Data: bytes.Repeat([]byte(" "), 120*40),
M internal/ports/remote.go +2 -1
@@ -12,6 +12,7 @@ import (
1212 "unicode/utf8"
1313
1414 "github.com/bnema/vev/internal/domain"
15+ "github.com/bnema/vev/internal/protocol"
1516 )
1617
1718 // RemoteTransportMode selects the transport used for remote attach after CLI parsing.
@@ -57,7 +58,7 @@ type RemoteCatalogClient interface {
5758 // RemotePreviewClient fetches one bounded, exact-target viewport without
5859 // attaching to or mutating the remote session.
5960 type RemotePreviewClient interface {
60- Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (RemotePreview, error)
61+ Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error)
6162 }
6263
6364 const (
M internal/ports/remote_preview_hardening_test.go +15 -14
@@ -8,6 +8,7 @@ import (
88
99 renderer "github.com/bnema/vev-vt"
1010 "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 const (
@@ -42,8 +43,8 @@ func TestRemotePreviewCodecGoldenRoundTrip(t *testing.T) {
4243 if err != nil {
4344 t.Fatalf("UnmarshalRemotePreview() error = %v", err)
4445 }
45- if got.Version != RemotePreviewSchemaVersion || got.Status != RemotePreviewOK {
46- t.Fatalf("header = (%d, %d), want (%d, %d)", got.Version, got.Status, RemotePreviewSchemaVersion, RemotePreviewOK)
46+ if got.Version != protocol.RemotePreviewSchemaVersion || got.Status != protocol.RemotePreviewOK {
47+ t.Fatalf("header = (%d, %d), want (%d, %d)", got.Version, got.Status, protocol.RemotePreviewSchemaVersion, protocol.RemotePreviewOK)
4748 }
4849 var wantLifecycle domain.SessionLifecycleID
4950 for i := range wantLifecycle {
@@ -86,19 +87,19 @@ func TestRemotePreviewCodecRejectsInvalidStatusFlagsAndRanges(t *testing.T) {
8687 func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {
8788 var lifecycle domain.SessionLifecycleID
8889 lifecycle[0] = 1
89- valid := RemotePreview{
90- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
90+ valid := protocol.RemotePreview{
91+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
9192 LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1,
9293 Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true}},
9394 }
9495 if MarshalRemotePreview(valid) == nil {
9596 t.Fatal("valid wide-cell preview did not marshal")
9697 }
97- for _, malformed := range []RemotePreview{
98- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: '界'}}},
99- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Continuation: true}, {Rune: 'x'}}},
100- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true, Rune: 'x'}}},
101- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 2, Cells: []renderer.Cell{{Rune: 'x'}, {Rune: '界'}, {Continuation: true}, {Rune: 'y'}}},
98+ for _, malformed := range []protocol.RemotePreview{
99+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: '界'}}},
100+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Continuation: true}, {Rune: 'x'}}},
101+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true, Rune: 'x'}}},
102+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 2, Cells: []renderer.Cell{{Rune: 'x'}, {Rune: '界'}, {Continuation: true}, {Rune: 'y'}}},
102103 } {
103104 if MarshalRemotePreview(malformed) != nil {
104105 t.Fatalf("malformed wide-cell preview marshaled: %#v", malformed.Cells)
@@ -117,16 +118,16 @@ func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {
117118 }
118119
119120 func TestRemotePreviewRejectsControlRunes(t *testing.T) {
120- valid := RemotePreview{
121- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
121+ valid := protocol.RemotePreview{
122+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
122123 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1,
123124 Cells: []renderer.Cell{{Rune: 'x'}},
124125 }
125126 for _, control := range []rune{'\x01', '\x1b', '\x7f', '\u0085'} {
126127 preview := valid
127128 preview.Cells = []renderer.Cell{{Rune: control}}
128- if err := ValidateRemotePreview(preview); err != ErrInvalidRemotePreview {
129- t.Fatalf("control rune %U: ValidateRemotePreview() error = %v, want %v", control, err, ErrInvalidRemotePreview)
129+ if err := protocol.ValidateRemotePreview(preview); err != protocol.ErrInvalidRemotePreview {
130+ t.Fatalf("control rune %U: ValidateRemotePreview() error = %v, want %v", control, err, protocol.ErrInvalidRemotePreview)
130131 }
131132 if MarshalRemotePreview(preview) != nil {
132133 t.Fatalf("control rune %U was marshaled", control)
@@ -137,7 +138,7 @@ func TestRemotePreviewRejectsControlRunes(t *testing.T) {
137138 func TestRemotePreviewRequestRejectsOversizedNestedRouteData(t *testing.T) {
138139 target := previewTargetForTest()
139140 target.Endpoint = strings.Repeat("h", math.MaxUint16+1)
140- request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
141+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
141142 if MarshalRemotePreviewRequest(request) != nil {
142143 t.Fatal("oversized endpoint was truncated and marshaled")
143144 }
M internal/ports/remote_preview_test.go +7 -6
@@ -5,6 +5,7 @@ import (
55
66 renderer "github.com/bnema/vev-vt"
77 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
89 )
910
1011 func previewTargetForTest() domain.RemoteSessionTarget {
@@ -19,8 +20,8 @@ func TestRemotePreviewCodecPreservesWideStyledCells(t *testing.T) {
1920 style.Attrs = renderer.AttrUnderline
2021 style.HasForegroundRGB = true
2122 style.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3}
22- preview := RemotePreview{
23- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
23+ preview := protocol.RemotePreview{
24+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
2425 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 4, Width: 3, Height: 1,
2526 Cells: []renderer.Cell{{Rune: '界', Style: style}, {Continuation: true, Style: style}, {Rune: 'x', Style: style}},
2627 }
@@ -38,7 +39,7 @@ func TestRemotePreviewCodecPreservesWideStyledCells(t *testing.T) {
3839 }
3940
4041 func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
41- preview := RemotePreview{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}}}
42+ preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}}}
4243 payload := MarshalRemotePreview(preview)
4344 for i := 0; i < len(payload); i++ {
4445 if _, err := UnmarshalRemotePreview(payload[:i]); err == nil {
@@ -49,7 +50,7 @@ func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
4950 t.Fatal("trailing garbage unexpectedly decoded")
5051 }
5152 bad := preview
52- bad.Width = RemotePreviewMaxWidth + 1
53+ bad.Width = protocol.RemotePreviewMaxWidth + 1
5354 if MarshalRemotePreview(bad) != nil {
5455 t.Fatal("oversized preview marshaled")
5556 }
@@ -57,14 +58,14 @@ func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
5758
5859 func TestRemotePreviewRequestRejectsStoppedAndInvalidTargets(t *testing.T) {
5960 target := previewTargetForTest()
60- request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
61+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
6162 if MarshalRemotePreviewRequest(request) == nil {
6263 t.Fatal("valid preview request did not marshal")
6364 }
6465 target.Stopped = true
6566 target.LiveTabID = ""
6667 target.StoppedTab = domain.NewOrdinalTabSelector(0, "", 1)
67- if MarshalRemotePreviewRequest(RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}) != nil {
68+ if MarshalRemotePreviewRequest(protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}) != nil {
6869 t.Fatal("stopped preview request marshaled")
6970 }
7071 }
M internal/ports/routes_test.go +52 -46
@@ -6,11 +6,12 @@ import (
66 "testing"
77
88 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/stretchr/testify/require"
1011 )
1112
12-func testExactTarget() ExactSessionTarget {
13- return ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}
13+func testExactTarget() protocol.ExactSessionTarget {
14+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}
1415 }
1516
1617 func TestExactSessionTargetCodecIsStrict(t *testing.T) {
@@ -26,8 +27,8 @@ func TestExactSessionTargetCodecIsStrict(t *testing.T) {
2627 require.NoError(t, err)
2728 require.Equal(t, want, got)
2829
29- _, err = MarshalExactSessionTarget(ExactSessionTarget{SessionName: "work"})
30- require.ErrorIs(t, err, ErrInvalidRouteWire)
30+ _, err = MarshalExactSessionTarget(protocol.ExactSessionTarget{SessionName: "work"})
31+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
3132 }
3233
3334 func TestHelloExactTargetRoundTrip(t *testing.T) {
@@ -57,7 +58,7 @@ func TestHelloExactTargetRoundTrip(t *testing.T) {
5758 }
5859
5960 func TestRoutePositionCodecIsStrict(t *testing.T) {
60- want := RoutePosition{Target: testExactTarget(), ActiveTabID: "tab-2"}
61+ want := protocol.RoutePosition{Target: testExactTarget(), ActiveTabID: "tab-2"}
6162 encoded, err := MarshalRoutePosition(want)
6263 require.NoError(t, err)
6364 require.Equal(t, "010203000000000000000000000000000004776f726b00057461622d32", hex.EncodeToString(encoded))
@@ -69,14 +70,14 @@ func TestRoutePositionCodecIsStrict(t *testing.T) {
6970 require.NoError(t, err)
7071 require.Equal(t, want, got)
7172
72- _, err = MarshalRoutePosition(RoutePosition{Target: testExactTarget()})
73- require.ErrorIs(t, err, ErrInvalidRouteWire)
74- _, err = MarshalRoutePosition(RoutePosition{Target: testExactTarget(), ActiveTabID: "bad tab"})
75- require.ErrorIs(t, err, ErrInvalidRouteWire)
73+ _, err = MarshalRoutePosition(protocol.RoutePosition{Target: testExactTarget()})
74+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
75+ _, err = MarshalRoutePosition(protocol.RoutePosition{Target: testExactTarget(), ActiveTabID: "bad tab"})
76+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
7677 }
7778
7879 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
79- identity := &CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
80+ identity := &protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
8081 want := Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}
8182
8283 got, err := UnmarshalWelcome(MarshalWelcome(want))
@@ -85,14 +86,14 @@ func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
8586 }
8687
8788 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {
88- identity := &CommittedRouteIdentity{Target: testExactTarget()}
89+ identity := &protocol.CommittedRouteIdentity{Target: testExactTarget()}
8990 payload := MarshalWelcome(Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})
9091
9192 require.Nil(t, payload)
9293 }
9394
9495 func TestCommittedRouteIdentityCodec(t *testing.T) {
95- want := CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
96+ want := protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
9697 encoded, err := MarshalCommittedRouteIdentity(want)
9798 require.NoError(t, err)
9899
@@ -111,17 +112,17 @@ func TestCommittedRouteIdentityCodec(t *testing.T) {
111112 require.Error(t, err)
112113 }
113114
114-func testRouteSnapshot() RecentRouteSnapshot {
115+func testRouteSnapshot() protocol.RecentRouteSnapshot {
115116 activeTarget := testExactTarget()
116- logsTarget := ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{4, 5, 6}, SessionName: "logs"}
117- return RecentRouteSnapshot{
117+ logsTarget := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{4, 5, 6}, SessionName: "logs"}
118+ return protocol.RecentRouteSnapshot{
118119 Generation: 9,
119- Active: RouteRef{Key: 11, Generation: 3},
120- ActiveEntry: RecentRouteEntry{Key: 11, Generation: 3, Target: activeTarget, Name: "work", Kind: RouteKindLocal, Reachability: RouteReachabilityUnknown},
121- Previous: RouteRef{Key: 12, Generation: 4},
122- Home: RouteRef{Key: 11, Generation: 3},
123- Entries: []RecentRouteEntry{
124- {Key: 12, Generation: 4, Target: logsTarget, Name: "logs", HostLabel: "edge", Kind: RouteKindRemote, Attention: true, Reachability: RouteReachabilityUnknown},
120+ Active: protocol.RouteRef{Key: 11, Generation: 3},
121+ ActiveEntry: protocol.RecentRouteEntry{Key: 11, Generation: 3, Target: activeTarget, Name: "work", Kind: protocol.RouteKindLocal, Reachability: protocol.RouteReachabilityUnknown},
122+ Previous: protocol.RouteRef{Key: 12, Generation: 4},
123+ Home: protocol.RouteRef{Key: 11, Generation: 3},
124+ Entries: []protocol.RecentRouteEntry{
125+ {Key: 12, Generation: 4, Target: logsTarget, Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote, Attention: true, Reachability: protocol.RouteReachabilityUnknown},
125126 },
126127 }
127128 }
@@ -142,36 +143,41 @@ func TestRecentRouteSnapshotCodecRoundTripAndBounds(t *testing.T) {
142143
143144 invalidCases := []struct {
144145 name string
145- mutate func(*RecentRouteSnapshot)
146+ mutate func(*protocol.RecentRouteSnapshot)
146147 }{
147- {name: "missing previous reference", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Previous = RouteRef{Key: 99, Generation: 99} }},
148- {name: "missing active presentation", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.ActiveEntry = RecentRouteEntry{} }},
149- {name: "route name differs from lifecycle target", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Entries[0].Name = "other" }},
150- {name: "entry is active", mutate: func(snapshot *RecentRouteSnapshot) {
148+ {name: "missing previous reference", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
149+ snapshot.Previous = protocol.RouteRef{Key: 99, Generation: 99}
150+ }},
151+ {name: "missing active presentation", mutate: func(snapshot *protocol.RecentRouteSnapshot) { snapshot.ActiveEntry = protocol.RecentRouteEntry{} }},
152+ {name: "route name differs from lifecycle target", mutate: func(snapshot *protocol.RecentRouteSnapshot) { snapshot.Entries[0].Name = "other" }},
153+ {name: "entry is active", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
151154 snapshot.Entries = append(snapshot.Entries, snapshot.ActiveEntry)
152155 }},
153- {name: "duplicate entry", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Entries = append(snapshot.Entries, snapshot.Entries[0]) }},
154- {name: "too many entries", mutate: func(snapshot *RecentRouteSnapshot) {
155- snapshot.Entries = make([]RecentRouteEntry, RouteSnapshotMaxEntries+1)
156+ {name: "duplicate entry", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
157+ snapshot.Entries = append(snapshot.Entries, snapshot.Entries[0])
158+ }},
159+ {name: "too many entries", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
160+ snapshot.Entries = make([]protocol.RecentRouteEntry, protocol.RouteSnapshotMaxEntries+1)
156161 for i := range snapshot.Entries {
157- target := ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{byte(i + 1)}, SessionName: "x"}
158- snapshot.Entries[i] = RecentRouteEntry{Key: uint64(i + 1), Generation: uint64(i + 1), Target: target, Name: "x", Kind: RouteKindLocal}
162+ target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{byte(i + 1)}, SessionName: "x"}
163+ snapshot.Entries[i] = protocol.RecentRouteEntry{Key: uint64(i + 1), Generation: uint64(i + 1), Target: target, Name: "x", Kind: protocol.RouteKindLocal}
159164 }
160165 }},
161166 }
162167 for _, tc := range invalidCases {
163168 t.Run(tc.name, func(t *testing.T) {
164169 invalid := want
170+ invalid.Entries = append([]protocol.RecentRouteEntry(nil), want.Entries...)
165171 tc.mutate(&invalid)
166172 _, err := MarshalRecentRouteSnapshot(invalid)
167- require.ErrorIs(t, err, ErrInvalidRouteWire)
173+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
168174 })
169175 }
170176 }
171177
172178 func TestRouteAttentionSubscriptionCodecIsStrict(t *testing.T) {
173- want := RouteAttentionSubscription{Targets: []RouteAttentionTarget{{
174- Ref: RouteRef{Key: 7, Generation: 3},
179+ want := protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{{
180+ Ref: protocol.RouteRef{Key: 7, Generation: 3},
175181 Target: testExactTarget(),
176182 }}}
177183 encoded, err := MarshalRouteAttentionSubscription(want)
@@ -185,15 +191,15 @@ func TestRouteAttentionSubscriptionCodecIsStrict(t *testing.T) {
185191 require.NoError(t, err)
186192 require.Equal(t, want, got)
187193
188- _, err = MarshalRouteAttentionSubscription(RouteAttentionSubscription{Targets: []RouteAttentionTarget{
194+ _, err = MarshalRouteAttentionSubscription(protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{
189195 want.Targets[0], want.Targets[0],
190196 }})
191- require.ErrorIs(t, err, ErrInvalidRouteWire)
197+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
192198 }
193199
194200 func TestRecentRouteSnapshotRejectsOversizedFrameBeforeParsing(t *testing.T) {
195201 _, err := UnmarshalRecentRouteSnapshot(make([]byte, MaxFrameLen))
196- require.ErrorIs(t, err, ErrInvalidRouteWire)
202+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
197203 }
198204
199205 func TestRouteLabelBoundsMatchRemoteDisplayOrigin(t *testing.T) {
@@ -207,7 +213,7 @@ func TestRouteLabelBoundsMatchRemoteDisplayOrigin(t *testing.T) {
207213 }
208214 for _, tt := range tests {
209215 t.Run(tt.name, func(t *testing.T) {
210- err := ValidateRouteLabel(strings.Repeat("a", tt.length), false)
216+ err := protocol.ValidateRouteLabel(strings.Repeat("a", tt.length), false)
211217 if tt.wantErr {
212218 require.Error(t, err)
213219 return
@@ -221,16 +227,16 @@ func TestRouteLabelsRejectTerminalUnsafeText(t *testing.T) {
221227 values := []string{"line\nfeed", "\u2028line-separator", "\u2029paragraph-separator", "\x1b[31mred", "\u202eoverride", string([]byte{0xff, 0xfe})}
222228 for _, value := range values {
223229 t.Run("name/"+value, func(t *testing.T) {
224- _, err := MarshalRecentRouteSnapshot(RecentRouteSnapshot{
230+ _, err := MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{
225231 Generation: 1,
226- Entries: []RecentRouteEntry{{Key: 1, Generation: 1, Target: ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: value}, Name: value, Kind: RouteKindLocal}},
232+ Entries: []protocol.RecentRouteEntry{{Key: 1, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: value}, Name: value, Kind: protocol.RouteKindLocal}},
227233 })
228234 require.Error(t, err)
229235 })
230236 t.Run("host/"+value, func(t *testing.T) {
231- _, err := MarshalRecentRouteSnapshot(RecentRouteSnapshot{
237+ _, err := MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{
232238 Generation: 1,
233- Entries: []RecentRouteEntry{{Key: 1, Generation: 1, Target: testExactTarget(), Name: "work", HostLabel: value, Kind: RouteKindRemote}},
239+ Entries: []protocol.RecentRouteEntry{{Key: 1, Generation: 1, Target: testExactTarget(), Name: "work", HostLabel: value, Kind: protocol.RouteKindRemote}},
234240 })
235241 require.Error(t, err)
236242 })
@@ -238,7 +244,7 @@ func TestRouteLabelsRejectTerminalUnsafeText(t *testing.T) {
238244 }
239245
240246 func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
241- action := RouteNavigationAction{SnapshotGeneration: 9, Key: 7, Generation: 8}
247+ action := protocol.RouteNavigationAction{SnapshotGeneration: 9, Key: 7, Generation: 8}
242248 encoded, err := MarshalRouteNavigationAction(action)
243249 require.NoError(t, err)
244250 require.Equal(t, "000000000000000900000000000000070000000000000008", hex.EncodeToString(encoded))
@@ -249,7 +255,7 @@ func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
249255 require.NoError(t, err)
250256 require.Equal(t, action, got)
251257
252- failure := RouteNavigationFailure{Key: 7, Generation: 8, Code: RouteFailureStaleSelection}
258+ failure := protocol.RouteNavigationFailure{Key: 7, Generation: 8, Code: protocol.RouteFailureStaleSelection}
253259 encoded, err = MarshalRouteNavigationFailure(failure)
254260 require.NoError(t, err)
255261 require.Equal(t, "0000000000000007000000000000000801", hex.EncodeToString(encoded))
@@ -260,11 +266,11 @@ func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
260266 require.NoError(t, err)
261267 require.Equal(t, failure, gotFailure)
262268
263- for _, invalid := range []RouteNavigationAction{{}, {SnapshotGeneration: 1, Key: 1}, {SnapshotGeneration: 1, Generation: 1}, {Key: 1, Generation: 1}} {
269+ for _, invalid := range []protocol.RouteNavigationAction{{}, {SnapshotGeneration: 1, Key: 1}, {SnapshotGeneration: 1, Generation: 1}, {Key: 1, Generation: 1}} {
264270 _, err := MarshalRouteNavigationAction(invalid)
265271 require.Error(t, err)
266272 }
267- for _, invalid := range []RouteNavigationFailure{{}, {Key: 1, Generation: 1}, {Key: 1, Generation: 1, Code: 255}} {
273+ for _, invalid := range []protocol.RouteNavigationFailure{{}, {Key: 1, Generation: 1}, {Key: 1, Generation: 1, Code: 255}} {
268274 _, err := MarshalRouteNavigationFailure(invalid)
269275 require.Error(t, err)
270276 }
M internal/ports/routes_wire.go +165 -167
@@ -1,34 +1,32 @@
11 package ports
22
33 import (
4- "errors"
54 "fmt"
65
76 "github.com/bnema/vev/internal/domain"
7+ "github.com/bnema/vev/internal/protocol"
88 )
99
10-var ErrInvalidRouteWire = errors.New("invalid route wire message")
11-
1210 func validateRouteString(value, field string, allowEmpty bool) error {
13- if err := ValidateRouteLabel(value, allowEmpty); err != nil {
11+ if err := protocol.ValidateRouteLabel(value, allowEmpty); err != nil {
1412 return fmt.Errorf("%s: %w", field, err)
1513 }
1614 return nil
1715 }
1816
19-func marshalExactSessionTarget(w *payloadWriter, target ExactSessionTarget) {
17+func marshalExactSessionTarget(w *payloadWriter, target protocol.ExactSessionTarget) {
2018 w.putBytes(target.LifecycleID[:])
2119 w.putString(target.SessionName)
2220 }
2321
24-func marshalExactTargetSection(w *payloadWriter, target *ExactSessionTarget) {
22+func marshalExactTargetSection(w *payloadWriter, target *protocol.ExactSessionTarget) {
2523 w.putBool(target != nil)
2624 if target != nil {
2725 marshalExactSessionTarget(w, *target)
2826 }
2927 }
3028
31-func unmarshalExactTargetSection(r *payloadReader) (*ExactSessionTarget, error) {
29+func unmarshalExactTargetSection(r *payloadReader) (*protocol.ExactSessionTarget, error) {
3230 present, err := r.getBool()
3331 if err != nil {
3432 return nil, err
@@ -57,27 +55,27 @@ func skipExactTargetSection(r *payloadReader) error {
5755 return nil
5856 }
5957
60-func unmarshalExactSessionTarget(r *payloadReader) (ExactSessionTarget, error) {
61- var target ExactSessionTarget
58+func unmarshalExactSessionTarget(r *payloadReader) (protocol.ExactSessionTarget, error) {
59+ var target protocol.ExactSessionTarget
6260 lifecycle, err := r.getBytes(len(target.LifecycleID))
6361 if err != nil {
64- return ExactSessionTarget{}, err
62+ return protocol.ExactSessionTarget{}, err
6563 }
6664 copy(target.LifecycleID[:], lifecycle)
6765 if target.SessionName, err = r.getString(); err != nil {
68- return ExactSessionTarget{}, err
66+ return protocol.ExactSessionTarget{}, err
6967 }
7068 if err := target.Validate(); err != nil {
71- return ExactSessionTarget{}, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)
69+ return protocol.ExactSessionTarget{}, fmt.Errorf("%w: exact target: %v", protocol.ErrInvalidRouteWire, err)
7270 }
7371 return target, nil
7472 }
7573
7674 // MarshalExactSessionTarget encodes the exact lifecycle/name pair used by
7775 // Hello and route-transition tests.
78-func MarshalExactSessionTarget(target ExactSessionTarget) ([]byte, error) {
76+func MarshalExactSessionTarget(target protocol.ExactSessionTarget) ([]byte, error) {
7977 if err := target.Validate(); err != nil {
80- return nil, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)
78+ return nil, fmt.Errorf("%w: exact target: %v", protocol.ErrInvalidRouteWire, err)
8179 }
8280 w := payloadWriter{}
8381 marshalExactSessionTarget(&w, target)
@@ -86,21 +84,21 @@ func MarshalExactSessionTarget(target ExactSessionTarget) ([]byte, error) {
8684
8785 // UnmarshalExactSessionTarget decodes one exact lifecycle/name pair and rejects
8886 // truncation, invalid identity, and trailing bytes.
89-func UnmarshalExactSessionTarget(b []byte) (ExactSessionTarget, error) {
87+func UnmarshalExactSessionTarget(b []byte) (protocol.ExactSessionTarget, error) {
9088 r := payloadReader{b: b}
9189 target, err := unmarshalExactSessionTarget(&r)
9290 if err != nil {
93- return ExactSessionTarget{}, err
91+ return protocol.ExactSessionTarget{}, err
9492 }
9593 if err := r.done(); err != nil {
96- return ExactSessionTarget{}, err
94+ return protocol.ExactSessionTarget{}, err
9795 }
9896 return target, nil
9997 }
10098
10199 // MarshalSamePeerSwitchRequest encodes the strict in-band session switch
102100 // confirmation. The target is non-optional and carries no display origin.
103-func MarshalSamePeerSwitchRequest(request SamePeerSwitchRequest) ([]byte, error) {
101+func MarshalSamePeerSwitchRequest(request protocol.SamePeerSwitchRequest) ([]byte, error) {
104102 if err := request.Validate(); err != nil {
105103 return nil, err
106104 }
@@ -112,32 +110,32 @@ func MarshalSamePeerSwitchRequest(request SamePeerSwitchRequest) ([]byte, error)
112110 }
113111
114112 // UnmarshalSamePeerSwitchRequest decodes one complete in-band switch request.
115-func UnmarshalSamePeerSwitchRequest(b []byte) (SamePeerSwitchRequest, error) {
113+func UnmarshalSamePeerSwitchRequest(b []byte) (protocol.SamePeerSwitchRequest, error) {
116114 r := payloadReader{b: b}
117- var request SamePeerSwitchRequest
115+ var request protocol.SamePeerSwitchRequest
118116 var err error
119117 if request.RequestID, err = r.getUint64(); err != nil {
120- return SamePeerSwitchRequest{}, err
118+ return protocol.SamePeerSwitchRequest{}, err
121119 }
122120 if request.Target, err = unmarshalExactSessionTarget(&r); err != nil {
123- return SamePeerSwitchRequest{}, err
121+ return protocol.SamePeerSwitchRequest{}, err
124122 }
125123 preferred, err := r.getString()
126124 if err != nil {
127- return SamePeerSwitchRequest{}, err
125+ return protocol.SamePeerSwitchRequest{}, err
128126 }
129127 request.PreferredTabID = domain.TabStableID(preferred)
130128 if err := r.done(); err != nil {
131- return SamePeerSwitchRequest{}, err
129+ return protocol.SamePeerSwitchRequest{}, err
132130 }
133131 if err := request.Validate(); err != nil {
134- return SamePeerSwitchRequest{}, err
132+ return protocol.SamePeerSwitchRequest{}, err
135133 }
136134 return request, nil
137135 }
138136
139137 // MarshalSamePeerSwitchFailure encodes a bounded pre-commit rejection.
140-func MarshalSamePeerSwitchFailure(failure SamePeerSwitchFailure) ([]byte, error) {
138+func MarshalSamePeerSwitchFailure(failure protocol.SamePeerSwitchFailure) ([]byte, error) {
141139 if err := failure.Validate(); err != nil {
142140 return nil, err
143141 }
@@ -148,37 +146,37 @@ func MarshalSamePeerSwitchFailure(failure SamePeerSwitchFailure) ([]byte, error)
148146 }
149147
150148 // UnmarshalSamePeerSwitchFailure decodes a complete bounded rejection.
151-func UnmarshalSamePeerSwitchFailure(b []byte) (SamePeerSwitchFailure, error) {
149+func UnmarshalSamePeerSwitchFailure(b []byte) (protocol.SamePeerSwitchFailure, error) {
152150 r := payloadReader{b: b}
153- var failure SamePeerSwitchFailure
151+ var failure protocol.SamePeerSwitchFailure
154152 var err error
155153 if failure.RequestID, err = r.getUint64(); err != nil {
156- return SamePeerSwitchFailure{}, err
154+ return protocol.SamePeerSwitchFailure{}, err
157155 }
158156 code, err := r.getUint8()
159157 if err != nil {
160- return SamePeerSwitchFailure{}, err
158+ return protocol.SamePeerSwitchFailure{}, err
161159 }
162- failure.Code = SamePeerSwitchFailureCode(code)
160+ failure.Code = protocol.SamePeerSwitchFailureCode(code)
163161 if err := r.done(); err != nil {
164- return SamePeerSwitchFailure{}, err
162+ return protocol.SamePeerSwitchFailure{}, err
165163 }
166164 if err := failure.Validate(); err != nil {
167- return SamePeerSwitchFailure{}, err
165+ return protocol.SamePeerSwitchFailure{}, err
168166 }
169167 return failure, nil
170168 }
171169
172-func validateCommittedRouteIdentity(identity CommittedRouteIdentity) error {
170+func validateCommittedRouteIdentity(identity protocol.CommittedRouteIdentity) error {
173171 if err := identity.Validate(); err != nil {
174- return fmt.Errorf("%w: committed identity: %v", ErrInvalidRouteWire, err)
172+ return fmt.Errorf("%w: committed identity: %v", protocol.ErrInvalidRouteWire, err)
175173 }
176174 return nil
177175 }
178176
179177 // MarshalCommittedRouteIdentity encodes the daemon's committed exact session
180178 // identity without exposing transport or client-ledger details.
181-func MarshalCommittedRouteIdentity(identity CommittedRouteIdentity) ([]byte, error) {
179+func MarshalCommittedRouteIdentity(identity protocol.CommittedRouteIdentity) ([]byte, error) {
182180 if err := validateCommittedRouteIdentity(identity); err != nil {
183181 return nil, err
184182 }
@@ -187,14 +185,14 @@ func MarshalCommittedRouteIdentity(identity CommittedRouteIdentity) ([]byte, err
187185 return w.b, nil
188186 }
189187
190-func marshalCommittedRouteIdentityFields(w *payloadWriter, identity CommittedRouteIdentity) {
188+func marshalCommittedRouteIdentityFields(w *payloadWriter, identity protocol.CommittedRouteIdentity) {
191189 marshalExactSessionTarget(w, identity.Target)
192190 w.putBool(identity.Ephemeral)
193191 }
194192
195193 // marshalCommittedIdentitySection shares the standalone identity body and
196194 // validates embedded identities before Welcome serialization.
197-func marshalCommittedIdentitySection(w *payloadWriter, identity *CommittedRouteIdentity) bool {
195+func marshalCommittedIdentitySection(w *payloadWriter, identity *protocol.CommittedRouteIdentity) bool {
198196 w.putBool(identity != nil)
199197 if identity == nil {
200198 return true
@@ -206,7 +204,7 @@ func marshalCommittedIdentitySection(w *payloadWriter, identity *CommittedRouteI
206204 return true
207205 }
208206
209-func unmarshalCommittedIdentitySection(r *payloadReader) (*CommittedRouteIdentity, error) {
207+func unmarshalCommittedIdentitySection(r *payloadReader) (*protocol.CommittedRouteIdentity, error) {
210208 present, err := r.getBool()
211209 if err != nil {
212210 return nil, err
@@ -222,38 +220,38 @@ func unmarshalCommittedIdentitySection(r *payloadReader) (*CommittedRouteIdentit
222220 if err != nil {
223221 return nil, err
224222 }
225- identity := &CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
223+ identity := &protocol.CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
226224 if err := validateCommittedRouteIdentity(*identity); err != nil {
227225 return nil, err
228226 }
229227 return identity, nil
230228 }
231229
232-func UnmarshalCommittedRouteIdentity(b []byte) (CommittedRouteIdentity, error) {
230+func UnmarshalCommittedRouteIdentity(b []byte) (protocol.CommittedRouteIdentity, error) {
233231 r := payloadReader{b: b}
234232 target, err := unmarshalExactSessionTarget(&r)
235233 if err != nil {
236- return CommittedRouteIdentity{}, err
234+ return protocol.CommittedRouteIdentity{}, err
237235 }
238236 ephemeral, err := r.getBool()
239237 if err != nil {
240- return CommittedRouteIdentity{}, err
238+ return protocol.CommittedRouteIdentity{}, err
241239 }
242240 if err := r.done(); err != nil {
243- return CommittedRouteIdentity{}, err
241+ return protocol.CommittedRouteIdentity{}, err
244242 }
245- identity := CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
243+ identity := protocol.CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
246244 if err := validateCommittedRouteIdentity(identity); err != nil {
247- return CommittedRouteIdentity{}, err
245+ return protocol.CommittedRouteIdentity{}, err
248246 }
249247 return identity, nil
250248 }
251249
252250 // MarshalRoutePosition encodes one exact session and its attachment-local tab
253251 // cursor. Route positions are mutable client memory, not attach authority.
254-func MarshalRoutePosition(position RoutePosition) ([]byte, error) {
252+func MarshalRoutePosition(position protocol.RoutePosition) ([]byte, error) {
255253 if err := position.Validate(); err != nil {
256- return nil, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)
254+ return nil, fmt.Errorf("%w: route position: %v", protocol.ErrInvalidRouteWire, err)
257255 }
258256 w := payloadWriter{}
259257 marshalExactSessionTarget(&w, position.Target)
@@ -262,85 +260,85 @@ func MarshalRoutePosition(position RoutePosition) ([]byte, error) {
262260 }
263261
264262 // UnmarshalRoutePosition decodes one strict route-position payload.
265-func UnmarshalRoutePosition(b []byte) (RoutePosition, error) {
263+func UnmarshalRoutePosition(b []byte) (protocol.RoutePosition, error) {
266264 r := payloadReader{b: b}
267265 target, err := unmarshalExactSessionTarget(&r)
268266 if err != nil {
269- return RoutePosition{}, err
267+ return protocol.RoutePosition{}, err
270268 }
271269 activeTabID, err := r.getString()
272270 if err != nil {
273- return RoutePosition{}, err
271+ return protocol.RoutePosition{}, err
274272 }
275273 if err := r.done(); err != nil {
276- return RoutePosition{}, err
274+ return protocol.RoutePosition{}, err
277275 }
278- position := RoutePosition{Target: target, ActiveTabID: domain.TabStableID(activeTabID)}
276+ position := protocol.RoutePosition{Target: target, ActiveTabID: domain.TabStableID(activeTabID)}
279277 if err := position.Validate(); err != nil {
280- return RoutePosition{}, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)
278+ return protocol.RoutePosition{}, fmt.Errorf("%w: route position: %v", protocol.ErrInvalidRouteWire, err)
281279 }
282280 return position, nil
283281 }
284282
285-func validateRouteRef(ref RouteRef) error {
283+func validateRouteRef(ref protocol.RouteRef) error {
286284 if err := ref.Validate(); err != nil {
287- return fmt.Errorf("%w: route reference: %v", ErrInvalidRouteWire, err)
285+ return fmt.Errorf("%w: route reference: %v", protocol.ErrInvalidRouteWire, err)
288286 }
289287 return nil
290288 }
291289
292-func marshalRouteRef(w *payloadWriter, ref RouteRef) {
290+func marshalRouteRef(w *payloadWriter, ref protocol.RouteRef) {
293291 w.putUint64(ref.Key)
294292 w.putUint64(ref.Generation)
295293 }
296294
297-func unmarshalRouteRef(r *payloadReader) (RouteRef, error) {
295+func unmarshalRouteRef(r *payloadReader) (protocol.RouteRef, error) {
298296 key, err := r.getUint64()
299297 if err != nil {
300- return RouteRef{}, err
298+ return protocol.RouteRef{}, err
301299 }
302300 generation, err := r.getUint64()
303301 if err != nil {
304- return RouteRef{}, err
302+ return protocol.RouteRef{}, err
305303 }
306- ref := RouteRef{Key: key, Generation: generation}
304+ ref := protocol.RouteRef{Key: key, Generation: generation}
307305 if err := validateRouteRef(ref); err != nil {
308- return RouteRef{}, err
306+ return protocol.RouteRef{}, err
309307 }
310308 return ref, nil
311309 }
312310
313-func validateRecentRouteEntry(entry RecentRouteEntry) error {
311+func validateRecentRouteEntry(entry protocol.RecentRouteEntry) error {
314312 if entry.Key == 0 || entry.Generation == 0 {
315- return fmt.Errorf("%w: route entry identity is zero", ErrInvalidRouteWire)
313+ return fmt.Errorf("%w: route entry identity is zero", protocol.ErrInvalidRouteWire)
316314 }
317315 if err := entry.Target.Validate(); err != nil {
318- return fmt.Errorf("%w: invalid route lifecycle target: %v", ErrInvalidRouteWire, err)
316+ return fmt.Errorf("%w: invalid route lifecycle target: %v", protocol.ErrInvalidRouteWire, err)
319317 }
320318 if entry.Name != entry.Target.SessionName {
321- return fmt.Errorf("%w: route name does not match lifecycle target", ErrInvalidRouteWire)
319+ return fmt.Errorf("%w: route name does not match lifecycle target", protocol.ErrInvalidRouteWire)
322320 }
323321 if err := validateRouteString(entry.Name, "route name", false); err != nil {
324- return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)
322+ return fmt.Errorf("%w: %v", protocol.ErrInvalidRouteWire, err)
325323 }
326324 if err := validateRouteString(entry.HostLabel, "route host label", true); err != nil {
327- return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)
325+ return fmt.Errorf("%w: %v", protocol.ErrInvalidRouteWire, err)
328326 }
329- if !entry.Kind.valid() {
330- return fmt.Errorf("%w: invalid route kind", ErrInvalidRouteWire)
327+ if entry.Kind.Validate() != nil {
328+ return fmt.Errorf("%w: invalid route kind", protocol.ErrInvalidRouteWire)
331329 }
332- if !entry.Reachability.valid() {
333- return fmt.Errorf("%w: invalid route reachability", ErrInvalidRouteWire)
330+ if entry.Reachability.Validate() != nil {
331+ return fmt.Errorf("%w: invalid route reachability", protocol.ErrInvalidRouteWire)
334332 }
335333 return nil
336334 }
337335
338-func validateRecentRouteSnapshot(snapshot RecentRouteSnapshot) error {
339- if len(snapshot.Entries) > RouteSnapshotMaxEntries {
340- return fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)
336+func validateRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) error {
337+ if len(snapshot.Entries) > protocol.RouteSnapshotMaxEntries {
338+ return fmt.Errorf("%w: too many route entries", protocol.ErrInvalidRouteWire)
341339 }
342- if snapshot.Generation == 0 && (len(snapshot.Entries) != 0 || snapshot.ActiveEntry != (RecentRouteEntry{})) {
343- return fmt.Errorf("%w: non-empty snapshot has zero generation", ErrInvalidRouteWire)
340+ if snapshot.Generation == 0 && (len(snapshot.Entries) != 0 || snapshot.ActiveEntry != (protocol.RecentRouteEntry{})) {
341+ return fmt.Errorf("%w: non-empty snapshot has zero generation", protocol.ErrInvalidRouteWire)
344342 }
345343 if err := validateRouteRef(snapshot.Active); err != nil {
346344 return err
@@ -351,55 +349,55 @@ func validateRecentRouteSnapshot(snapshot RecentRouteSnapshot) error {
351349 if err := validateRouteRef(snapshot.Home); err != nil {
352350 return err
353351 }
354- if snapshot.Active.empty() {
355- if snapshot.ActiveEntry != (RecentRouteEntry{}) {
356- return fmt.Errorf("%w: active presentation has no active route", ErrInvalidRouteWire)
352+ if snapshot.Active.IsZero() {
353+ if snapshot.ActiveEntry != (protocol.RecentRouteEntry{}) {
354+ return fmt.Errorf("%w: active presentation has no active route", protocol.ErrInvalidRouteWire)
357355 }
358356 } else {
359357 if err := validateRecentRouteEntry(snapshot.ActiveEntry); err != nil {
360358 return err
361359 }
362- if snapshot.Active != (RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation}) {
363- return fmt.Errorf("%w: active presentation does not match active route", ErrInvalidRouteWire)
360+ if snapshot.Active != (protocol.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation}) {
361+ return fmt.Errorf("%w: active presentation does not match active route", protocol.ErrInvalidRouteWire)
364362 }
365363 }
366- if !snapshot.Active.empty() && snapshot.Active == snapshot.Previous {
367- return fmt.Errorf("%w: active and previous routes are identical", ErrInvalidRouteWire)
364+ if !snapshot.Active.IsZero() && snapshot.Active == snapshot.Previous {
365+ return fmt.Errorf("%w: active and previous routes are identical", protocol.ErrInvalidRouteWire)
368366 }
369- refs := make(map[RouteRef]struct{}, len(snapshot.Entries))
367+ refs := make(map[protocol.RouteRef]struct{}, len(snapshot.Entries))
370368 for _, entry := range snapshot.Entries {
371- if snapshot.Active == (RouteRef{Key: entry.Key, Generation: entry.Generation}) {
372- return fmt.Errorf("%w: active route must be metadata-only", ErrInvalidRouteWire)
369+ if snapshot.Active == (protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}) {
370+ return fmt.Errorf("%w: active route must be metadata-only", protocol.ErrInvalidRouteWire)
373371 }
374372 if err := validateRecentRouteEntry(entry); err != nil {
375373 return err
376374 }
377- ref := RouteRef{Key: entry.Key, Generation: entry.Generation}
375+ ref := protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
378376 if _, exists := refs[ref]; exists {
379- return fmt.Errorf("%w: duplicate route entry", ErrInvalidRouteWire)
377+ return fmt.Errorf("%w: duplicate route entry", protocol.ErrInvalidRouteWire)
380378 }
381379 refs[ref] = struct{}{}
382380 }
383381 for _, item := range []struct {
384382 name string
385- ref RouteRef
383+ ref protocol.RouteRef
386384 }{
387385 {name: "previous", ref: snapshot.Previous},
388386 {name: "home", ref: snapshot.Home},
389387 } {
390- if item.ref.empty() || item.ref == snapshot.Active {
388+ if item.ref.IsZero() || item.ref == snapshot.Active {
391389 // Active is metadata-only and intentionally excluded from the
392390 // recent display entries. Home may point at active as well.
393391 continue
394392 }
395393 if _, exists := refs[item.ref]; !exists {
396- return fmt.Errorf("%w: %s route reference is absent from snapshot", ErrInvalidRouteWire, item.name)
394+ return fmt.Errorf("%w: %s route reference is absent from snapshot", protocol.ErrInvalidRouteWire, item.name)
397395 }
398396 }
399397 return nil
400398 }
401399
402-func marshalRecentRouteEntry(w *payloadWriter, entry RecentRouteEntry) {
400+func marshalRecentRouteEntry(w *payloadWriter, entry protocol.RecentRouteEntry) {
403401 w.putUint64(entry.Key)
404402 w.putUint64(entry.Generation)
405403 marshalExactSessionTarget(w, entry.Target)
@@ -411,52 +409,52 @@ func marshalRecentRouteEntry(w *payloadWriter, entry RecentRouteEntry) {
411409 w.putUint8(uint8(entry.Reachability))
412410 }
413411
414-func unmarshalRecentRouteEntry(r *payloadReader) (RecentRouteEntry, error) {
415- var entry RecentRouteEntry
412+func unmarshalRecentRouteEntry(r *payloadReader) (protocol.RecentRouteEntry, error) {
413+ var entry protocol.RecentRouteEntry
416414 var err error
417415 if entry.Key, err = r.getUint64(); err != nil {
418- return RecentRouteEntry{}, err
416+ return protocol.RecentRouteEntry{}, err
419417 }
420418 if entry.Generation, err = r.getUint64(); err != nil {
421- return RecentRouteEntry{}, err
419+ return protocol.RecentRouteEntry{}, err
422420 }
423421 if entry.Target, err = unmarshalExactSessionTarget(r); err != nil {
424- return RecentRouteEntry{}, err
422+ return protocol.RecentRouteEntry{}, err
425423 }
426424 if entry.Name, err = r.getString(); err != nil {
427- return RecentRouteEntry{}, err
425+ return protocol.RecentRouteEntry{}, err
428426 }
429427 if entry.HostLabel, err = r.getString(); err != nil {
430- return RecentRouteEntry{}, err
428+ return protocol.RecentRouteEntry{}, err
431429 }
432430 kind, err := r.getUint8()
433431 if err != nil {
434- return RecentRouteEntry{}, err
432+ return protocol.RecentRouteEntry{}, err
435433 }
436- entry.Kind = RouteKind(kind)
434+ entry.Kind = protocol.RouteKind(kind)
437435 if entry.Ephemeral, err = r.getBool(); err != nil {
438- return RecentRouteEntry{}, err
436+ return protocol.RecentRouteEntry{}, err
439437 }
440438 if entry.Attention, err = r.getBool(); err != nil {
441- return RecentRouteEntry{}, err
439+ return protocol.RecentRouteEntry{}, err
442440 }
443441 reachability, err := r.getUint8()
444442 if err != nil {
445- return RecentRouteEntry{}, err
443+ return protocol.RecentRouteEntry{}, err
446444 }
447- entry.Reachability = RouteReachability(reachability)
445+ entry.Reachability = protocol.RouteReachability(reachability)
448446 return entry, nil
449447 }
450448
451449 // MarshalRecentRouteSnapshot encodes a complete bounded client route view.
452-func MarshalRecentRouteSnapshot(snapshot RecentRouteSnapshot) ([]byte, error) {
450+func MarshalRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) ([]byte, error) {
453451 if err := validateRecentRouteSnapshot(snapshot); err != nil {
454452 return nil, err
455453 }
456454 w := payloadWriter{}
457455 w.putUint64(snapshot.Generation)
458456 marshalRouteRef(&w, snapshot.Active)
459- if !snapshot.Active.empty() {
457+ if !snapshot.Active.IsZero() {
460458 marshalRecentRouteEntry(&w, snapshot.ActiveEntry)
461459 }
462460 marshalRouteRef(&w, snapshot.Previous)
@@ -469,25 +467,25 @@ func MarshalRecentRouteSnapshot(snapshot RecentRouteSnapshot) ([]byte, error) {
469467 // make this unreachable; it protects the wire contract if those bounds
470468 // change independently in a future protocol revision.
471469 if len(w.b) > MaxFrameLen-1 {
472- return nil, fmt.Errorf("%w: route snapshot is too large", ErrInvalidRouteWire)
470+ return nil, fmt.Errorf("%w: route snapshot is too large", protocol.ErrInvalidRouteWire)
473471 }
474472 return w.b, nil
475473 }
476474
477-func validateRouteAttentionSubscription(subscription RouteAttentionSubscription) error {
478- if len(subscription.Targets) > RouteSnapshotMaxEntries {
479- return fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)
475+func validateRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) error {
476+ if len(subscription.Targets) > protocol.RouteSnapshotMaxEntries {
477+ return fmt.Errorf("%w: too many attention targets", protocol.ErrInvalidRouteWire)
480478 }
481- refs := make(map[RouteRef]struct{}, len(subscription.Targets))
479+ refs := make(map[protocol.RouteRef]struct{}, len(subscription.Targets))
482480 for _, target := range subscription.Targets {
483- if err := validateRouteRef(target.Ref); err != nil || target.Ref.empty() {
484- return fmt.Errorf("%w: invalid attention route reference", ErrInvalidRouteWire)
481+ if err := validateRouteRef(target.Ref); err != nil || target.Ref.IsZero() {
482+ return fmt.Errorf("%w: invalid attention route reference", protocol.ErrInvalidRouteWire)
485483 }
486484 if err := target.Target.Validate(); err != nil {
487- return fmt.Errorf("%w: invalid attention target: %v", ErrInvalidRouteWire, err)
485+ return fmt.Errorf("%w: invalid attention target: %v", protocol.ErrInvalidRouteWire, err)
488486 }
489487 if _, exists := refs[target.Ref]; exists {
490- return fmt.Errorf("%w: duplicate attention route reference", ErrInvalidRouteWire)
488+ return fmt.Errorf("%w: duplicate attention route reference", protocol.ErrInvalidRouteWire)
491489 }
492490 refs[target.Ref] = struct{}{}
493491 }
@@ -496,7 +494,7 @@ func validateRouteAttentionSubscription(subscription RouteAttentionSubscription)
496494
497495 // MarshalRouteAttentionSubscription encodes the bounded client mapping used
498496 // by the connected daemon to resolve live route-attention indicators.
499-func MarshalRouteAttentionSubscription(subscription RouteAttentionSubscription) ([]byte, error) {
497+func MarshalRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) ([]byte, error) {
500498 if err := validateRouteAttentionSubscription(subscription); err != nil {
501499 return nil, err
502500 }
@@ -511,96 +509,96 @@ func MarshalRouteAttentionSubscription(subscription RouteAttentionSubscription)
511509
512510 // UnmarshalRouteAttentionSubscription decodes one strict bounded attention
513511 // mapping and rejects truncated, malformed, and trailing payloads.
514-func UnmarshalRouteAttentionSubscription(b []byte) (RouteAttentionSubscription, error) {
512+func UnmarshalRouteAttentionSubscription(b []byte) (protocol.RouteAttentionSubscription, error) {
515513 r := payloadReader{b: b}
516514 count, err := r.getUint8()
517515 if err != nil {
518- return RouteAttentionSubscription{}, err
516+ return protocol.RouteAttentionSubscription{}, err
519517 }
520- if int(count) > RouteSnapshotMaxEntries {
521- return RouteAttentionSubscription{}, fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)
518+ if int(count) > protocol.RouteSnapshotMaxEntries {
519+ return protocol.RouteAttentionSubscription{}, fmt.Errorf("%w: too many attention targets", protocol.ErrInvalidRouteWire)
522520 }
523- subscription := RouteAttentionSubscription{Targets: make([]RouteAttentionTarget, 0, int(count))}
521+ subscription := protocol.RouteAttentionSubscription{Targets: make([]protocol.RouteAttentionTarget, 0, int(count))}
524522 for range int(count) {
525523 ref, err := unmarshalRouteRef(&r)
526524 if err != nil {
527- return RouteAttentionSubscription{}, err
525+ return protocol.RouteAttentionSubscription{}, err
528526 }
529527 target, err := unmarshalExactSessionTarget(&r)
530528 if err != nil {
531- return RouteAttentionSubscription{}, err
529+ return protocol.RouteAttentionSubscription{}, err
532530 }
533- subscription.Targets = append(subscription.Targets, RouteAttentionTarget{Ref: ref, Target: target})
531+ subscription.Targets = append(subscription.Targets, protocol.RouteAttentionTarget{Ref: ref, Target: target})
534532 }
535533 if err := r.done(); err != nil {
536- return RouteAttentionSubscription{}, err
534+ return protocol.RouteAttentionSubscription{}, err
537535 }
538536 if err := validateRouteAttentionSubscription(subscription); err != nil {
539- return RouteAttentionSubscription{}, err
537+ return protocol.RouteAttentionSubscription{}, err
540538 }
541539 return subscription, nil
542540 }
543541
544542 // UnmarshalRecentRouteSnapshot decodes and strictly validates one complete
545543 // immutable snapshot, including every referenced active/previous/home entry.
546-func UnmarshalRecentRouteSnapshot(b []byte) (RecentRouteSnapshot, error) {
544+func UnmarshalRecentRouteSnapshot(b []byte) (protocol.RecentRouteSnapshot, error) {
547545 if len(b) > MaxFrameLen-1 {
548- return RecentRouteSnapshot{}, ErrInvalidRouteWire
546+ return protocol.RecentRouteSnapshot{}, protocol.ErrInvalidRouteWire
549547 }
550548 r := payloadReader{b: b}
551- var snapshot RecentRouteSnapshot
549+ var snapshot protocol.RecentRouteSnapshot
552550 var err error
553551 if snapshot.Generation, err = r.getUint64(); err != nil {
554- return RecentRouteSnapshot{}, err
552+ return protocol.RecentRouteSnapshot{}, err
555553 }
556554 if snapshot.Active, err = unmarshalRouteRef(&r); err != nil {
557- return RecentRouteSnapshot{}, err
555+ return protocol.RecentRouteSnapshot{}, err
558556 }
559- if !snapshot.Active.empty() {
557+ if !snapshot.Active.IsZero() {
560558 if snapshot.ActiveEntry, err = unmarshalRecentRouteEntry(&r); err != nil {
561- return RecentRouteSnapshot{}, err
559+ return protocol.RecentRouteSnapshot{}, err
562560 }
563561 }
564562 if snapshot.Previous, err = unmarshalRouteRef(&r); err != nil {
565- return RecentRouteSnapshot{}, err
563+ return protocol.RecentRouteSnapshot{}, err
566564 }
567565 if snapshot.Home, err = unmarshalRouteRef(&r); err != nil {
568- return RecentRouteSnapshot{}, err
566+ return protocol.RecentRouteSnapshot{}, err
569567 }
570568 count, err := r.getUint8()
571569 if err != nil {
572- return RecentRouteSnapshot{}, err
570+ return protocol.RecentRouteSnapshot{}, err
573571 }
574- if int(count) > RouteSnapshotMaxEntries {
575- return RecentRouteSnapshot{}, fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)
572+ if int(count) > protocol.RouteSnapshotMaxEntries {
573+ return protocol.RecentRouteSnapshot{}, fmt.Errorf("%w: too many route entries", protocol.ErrInvalidRouteWire)
576574 }
577575 if count != 0 {
578- snapshot.Entries = make([]RecentRouteEntry, 0, int(count))
576+ snapshot.Entries = make([]protocol.RecentRouteEntry, 0, int(count))
579577 }
580578 for range int(count) {
581579 entry, err := unmarshalRecentRouteEntry(&r)
582580 if err != nil {
583- return RecentRouteSnapshot{}, err
581+ return protocol.RecentRouteSnapshot{}, err
584582 }
585583 snapshot.Entries = append(snapshot.Entries, entry)
586584 }
587585 if err := r.done(); err != nil {
588- return RecentRouteSnapshot{}, err
586+ return protocol.RecentRouteSnapshot{}, err
589587 }
590588 if err := validateRecentRouteSnapshot(snapshot); err != nil {
591- return RecentRouteSnapshot{}, err
589+ return protocol.RecentRouteSnapshot{}, err
592590 }
593591 return snapshot, nil
594592 }
595593
596-func validateRouteNavigationAction(action RouteNavigationAction) error {
594+func validateRouteNavigationAction(action protocol.RouteNavigationAction) error {
597595 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
598- return fmt.Errorf("%w: navigation action identity is zero", ErrInvalidRouteWire)
596+ return fmt.Errorf("%w: navigation action identity is zero", protocol.ErrInvalidRouteWire)
599597 }
600598 return nil
601599 }
602600
603-func MarshalRouteNavigationAction(action RouteNavigationAction) ([]byte, error) {
601+func MarshalRouteNavigationAction(action protocol.RouteNavigationAction) ([]byte, error) {
604602 if err := validateRouteNavigationAction(action); err != nil {
605603 return nil, err
606604 }
@@ -611,39 +609,39 @@ func MarshalRouteNavigationAction(action RouteNavigationAction) ([]byte, error)
611609 return w.b, nil
612610 }
613611
614-func UnmarshalRouteNavigationAction(b []byte) (RouteNavigationAction, error) {
612+func UnmarshalRouteNavigationAction(b []byte) (protocol.RouteNavigationAction, error) {
615613 r := payloadReader{b: b}
616- var action RouteNavigationAction
614+ var action protocol.RouteNavigationAction
617615 var err error
618616 if action.SnapshotGeneration, err = r.getUint64(); err != nil {
619- return RouteNavigationAction{}, err
617+ return protocol.RouteNavigationAction{}, err
620618 }
621619 if action.Key, err = r.getUint64(); err != nil {
622- return RouteNavigationAction{}, err
620+ return protocol.RouteNavigationAction{}, err
623621 }
624622 if action.Generation, err = r.getUint64(); err != nil {
625- return RouteNavigationAction{}, err
623+ return protocol.RouteNavigationAction{}, err
626624 }
627625 if err := r.done(); err != nil {
628- return RouteNavigationAction{}, err
626+ return protocol.RouteNavigationAction{}, err
629627 }
630628 if err := validateRouteNavigationAction(action); err != nil {
631- return RouteNavigationAction{}, err
629+ return protocol.RouteNavigationAction{}, err
632630 }
633631 return action, nil
634632 }
635633
636-func validateRouteNavigationFailure(failure RouteNavigationFailure) error {
634+func validateRouteNavigationFailure(failure protocol.RouteNavigationFailure) error {
637635 if failure.Key == 0 || failure.Generation == 0 {
638- return fmt.Errorf("%w: failure identity is zero", ErrInvalidRouteWire)
636+ return fmt.Errorf("%w: failure identity is zero", protocol.ErrInvalidRouteWire)
639637 }
640- if !failure.Code.valid() {
641- return fmt.Errorf("%w: invalid failure code", ErrInvalidRouteWire)
638+ if failure.Code.Validate() != nil {
639+ return fmt.Errorf("%w: invalid failure code", protocol.ErrInvalidRouteWire)
642640 }
643641 return nil
644642 }
645643
646-func MarshalRouteNavigationFailure(failure RouteNavigationFailure) ([]byte, error) {
644+func MarshalRouteNavigationFailure(failure protocol.RouteNavigationFailure) ([]byte, error) {
647645 if err := validateRouteNavigationFailure(failure); err != nil {
648646 return nil, err
649647 }
@@ -654,26 +652,26 @@ func MarshalRouteNavigationFailure(failure RouteNavigationFailure) ([]byte, erro
654652 return w.b, nil
655653 }
656654
657-func UnmarshalRouteNavigationFailure(b []byte) (RouteNavigationFailure, error) {
655+func UnmarshalRouteNavigationFailure(b []byte) (protocol.RouteNavigationFailure, error) {
658656 r := payloadReader{b: b}
659- var failure RouteNavigationFailure
657+ var failure protocol.RouteNavigationFailure
660658 var err error
661659 if failure.Key, err = r.getUint64(); err != nil {
662- return RouteNavigationFailure{}, err
660+ return protocol.RouteNavigationFailure{}, err
663661 }
664662 if failure.Generation, err = r.getUint64(); err != nil {
665- return RouteNavigationFailure{}, err
663+ return protocol.RouteNavigationFailure{}, err
666664 }
667665 code, err := r.getUint8()
668666 if err != nil {
669- return RouteNavigationFailure{}, err
667+ return protocol.RouteNavigationFailure{}, err
670668 }
671- failure.Code = RouteFailureCode(code)
669+ failure.Code = protocol.RouteFailureCode(code)
672670 if err := r.done(); err != nil {
673- return RouteNavigationFailure{}, err
671+ return protocol.RouteNavigationFailure{}, err
674672 }
675673 if err := validateRouteNavigationFailure(failure); err != nil {
676- return RouteNavigationFailure{}, err
674+ return protocol.RouteNavigationFailure{}, err
677675 }
678676 return failure, nil
679677 }
M internal/ports/same_peer_switch_test.go +17 -8
@@ -5,19 +5,20 @@ import (
55 "testing"
66
77 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
89 )
910
10-func samePeerSwitchTarget() ExactSessionTarget {
11+func samePeerSwitchTarget() protocol.ExactSessionTarget {
1112 var lifecycle domain.SessionLifecycleID
1213 lifecycle[0] = 9
13- return ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
14+ return protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
1415 }
1516
1617 func TestSamePeerSwitchWireStrict(t *testing.T) {
17- request := SamePeerSwitchRequest{RequestID: 7, Target: samePeerSwitchTarget(), PreferredTabID: "tab-2"}
18+ request := protocol.SamePeerSwitchRequest{RequestID: 7, Target: samePeerSwitchTarget(), PreferredTabID: "tab-2"}
1819 requestWire := append([]byte{0, 0, 0, 0, 0, 0, 0, 7, 9}, make([]byte, 15)...)
1920 requestWire = append(requestWire, 0, 4, 'w', 'o', 'r', 'k', 0, 5, 't', 'a', 'b', '-', '2')
20- failure := SamePeerSwitchFailure{RequestID: 7, Code: SamePeerSwitchStaleTarget}
21+ failure := protocol.SamePeerSwitchFailure{RequestID: 7, Code: protocol.SamePeerSwitchStaleTarget}
2122
2223 for _, tt := range []struct {
2324 name string
@@ -33,7 +34,11 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
3334 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchRequest(payload) },
3435 assert: func(t *testing.T, decoded any) {
3536 t.Helper()
36- if got := decoded.(SamePeerSwitchRequest); got != request {
37+ got, ok := decoded.(protocol.SamePeerSwitchRequest)
38+ if !ok {
39+ t.Fatalf("decoded type = %T, want protocol.SamePeerSwitchRequest", decoded)
40+ }
41+ if got != request {
3742 t.Fatalf("request = %+v, want %+v", got, request)
3843 }
3944 },
@@ -45,7 +50,11 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
4550 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchFailure(payload) },
4651 assert: func(t *testing.T, decoded any) {
4752 t.Helper()
48- if got := decoded.(SamePeerSwitchFailure); got != failure {
53+ got, ok := decoded.(protocol.SamePeerSwitchFailure)
54+ if !ok {
55+ t.Fatalf("decoded type = %T, want protocol.SamePeerSwitchFailure", decoded)
56+ }
57+ if got != failure {
4958 t.Fatalf("failure = %+v, want %+v", got, failure)
5059 }
5160 },
@@ -71,8 +80,8 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
7180 }
7281
7382 func TestSamePeerSwitchWireRejectsInvalidValues(t *testing.T) {
74- valid := SamePeerSwitchRequest{RequestID: 1, Target: samePeerSwitchTarget()}
75- for _, request := range []SamePeerSwitchRequest{
83+ valid := protocol.SamePeerSwitchRequest{RequestID: 1, Target: samePeerSwitchTarget()}
84+ for _, request := range []protocol.SamePeerSwitchRequest{
7685 {},
7786 {RequestID: 1},
7887 {RequestID: 1, Target: samePeerSwitchTarget(), PreferredTabID: "bad tab"},
A internal/ports/session_control.go +323 -0
@@ -0,0 +1,323 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "math"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/protocol"
11+)
12+
13+// HandshakeTimeout bounds every transport handshake from connect through the
14+// first committed publication.
15+const HandshakeTimeout = 15 * time.Second
16+
17+// ProtocolVersion is the current vev IPC wire protocol version.
18+const ProtocolVersion uint16 = 37
19+
20+// ConnectionCapabilities describes transport behavior relevant to session
21+// flow without naming a concrete carriage.
22+type ConnectionCapabilities struct {
23+ OutputDataLimit int
24+ PreferredOutputWindow uint8
25+ AsyncSend bool
26+ OwnedSynchronousSend bool
27+ LinkState bool
28+}
29+
30+var (
31+ ErrInvalidHello = errors.New("ports: invalid hello")
32+ ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
33+ ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
34+ ErrInvalidClientNotice = errors.New("ports: invalid client notice")
35+ ErrInvalidDetached = errors.New("ports: invalid detached reason")
36+)
37+
38+const (
39+ IntentEphemeral uint8 = 0
40+ IntentNew uint8 = 1
41+ IntentAttach uint8 = 2
42+ IntentResume uint8 = 3
43+)
44+
45+type EnvironmentPolicy uint8
46+
47+const (
48+ EnvironmentPolicyClientOwned EnvironmentPolicy = iota
49+ EnvironmentPolicyDaemonOwned
50+)
51+
52+const (
53+ CapabilityResume uint32 = 1 << 0
54+ CapabilityUDP uint32 = 1 << 1
55+ CapabilityPredict uint32 = 1 << 2
56+)
57+
58+const (
59+ ErrVersionMismatch uint16 = 1
60+ ErrNoSuchSession uint16 = 2
61+ ErrNameTaken uint16 = 3
62+ ErrServerShutdown uint16 = 4
63+ ErrInvalidSessionName uint16 = 5
64+ ErrUnknownCommand uint16 = 6
65+ ErrNotScriptable uint16 = 7
66+ ErrInvalidCommandArgs uint16 = 8
67+ ErrNoSuchTarget uint16 = 9
68+ ErrAmbiguousTarget uint16 = 10
69+ ErrInternal uint16 = 255
70+)
71+
72+const (
73+ ReasonDetach uint8 = 0
74+ ReasonSessionKilled uint8 = 1
75+ ReasonServerShutdown uint8 = 2
76+ ReasonReplaced uint8 = 3
77+)
78+
79+// Hello is sent by the client immediately after connecting.
80+type Hello struct {
81+ Version uint16
82+ Intent uint8
83+ ClientID [16]byte
84+ ResumeToken uint64
85+ Name string
86+ Size domain.Size
87+ PixelWidth int
88+ PixelHeight int
89+ TermEnv string
90+ Cwd string
91+ TrueColor bool
92+ // KittyDirectGraphics is an explicit declaration that the client has
93+ // probed its direct outer terminal and it accepts Kitty graphics output.
94+ KittyDirectGraphics bool
95+ MaxOutputInFlight uint8
96+ Env []string
97+ RemoteTarget *domain.RemoteSessionTarget
98+ ExactTarget *protocol.ExactSessionTarget
99+ PreferredTabID domain.TabStableID
100+ EnvironmentPolicy EnvironmentPolicy
101+ NavigationCapabilities protocol.NavigationCapabilities
102+ StartupOverlay protocol.StartupOverlay
103+ Remote bool
104+}
105+
106+func (h Hello) Geometry() domain.Geometry {
107+ return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
108+}
109+
110+type Input struct {
111+ InputSeq uint64
112+ Data []byte
113+}
114+
115+type ClientNotice struct{ Action uint8 }
116+
117+const (
118+ ClientNoticeClipboardFallback uint8 = 1
119+ ClientNoticeClipboardTooLarge uint8 = 2
120+ ClientNoticeLinkDegraded uint8 = 3
121+ ClientNoticeLinkConnected uint8 = 4
122+)
123+
124+type Detach struct{}
125+type Ping struct{}
126+type Pong struct{}
127+
128+type Welcome struct {
129+ SessionID string
130+ SessionName string
131+ Ephemeral bool
132+ ResumeToken uint64
133+ Capabilities uint32
134+ CommittedIdentity *protocol.CommittedRouteIdentity
135+}
136+
137+type ErrorMsg struct {
138+ Code uint16
139+ Text string
140+}
141+
142+type AttachTarget struct {
143+ Endpoint string
144+ Session string
145+ Intent uint8
146+ RemoteTarget *domain.RemoteSessionTarget
147+ ExactTarget *protocol.ExactSessionTarget
148+ EnvironmentPolicy EnvironmentPolicy
149+ SamePeer bool
150+}
151+
152+type Detached struct{ Reason uint8 }
153+type List struct{}
154+
155+type Kill struct {
156+ Name string
157+ All bool
158+}
159+
160+type SessionState uint8
161+
162+const (
163+ SessionUp SessionState = iota
164+ SessionDown
165+ SessionBroken
166+)
167+
168+type SessionInfo struct {
169+ SessionID string
170+ Name string
171+ State SessionState
172+ Ephemeral bool
173+ Tabs uint16
174+ Attached bool
175+}
176+
177+type Sessions struct{ Sessions []SessionInfo }
178+type OutputResetRequest struct{}
179+
180+func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
181+ return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
182+}
183+
184+func validClientNoticeAction(action uint8) bool {
185+ switch action {
186+ case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
187+ return true
188+ default:
189+ return false
190+ }
191+}
192+
193+func validDetachedReason(reason uint8) bool {
194+ switch reason {
195+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
196+ return true
197+ default:
198+ return false
199+ }
200+}
201+
202+func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
203+ if !validEnvironmentPolicy(policy) {
204+ return ErrInvalidEnvironmentPolicy
205+ }
206+ return nil
207+}
208+
209+func ValidateClientNotice(m ClientNotice) error {
210+ if !validClientNoticeAction(m.Action) {
211+ return ErrInvalidClientNotice
212+ }
213+ return nil
214+}
215+
216+func ValidateDetached(m Detached) error {
217+ if !validDetachedReason(m.Reason) {
218+ return ErrInvalidDetached
219+ }
220+ return nil
221+}
222+
223+func validateHelloNavigation(h Hello) error {
224+ if h.Intent != IntentAttach && h.Intent != IntentResume {
225+ if h.NavigationCapabilities != 0 || h.StartupOverlay != protocol.StartupOverlayNone {
226+ return protocol.ErrInvalidNavigation
227+ }
228+ return nil
229+ }
230+ return protocol.ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
231+}
232+
233+func ValidateHello(h Hello) error {
234+ if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
235+ return ErrInvalidHello
236+ }
237+ if err := protocol.ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
238+ return fmt.Errorf("%w: geometry", ErrInvalidHello)
239+ }
240+ if !validEnvironmentPolicy(h.EnvironmentPolicy) {
241+ return ErrInvalidHello
242+ }
243+ if err := validateHelloNavigation(h); err != nil {
244+ return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
245+ }
246+ if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
247+ return ErrInvalidHello
248+ }
249+ for _, entry := range h.Env {
250+ if uint64(len(entry)) > math.MaxUint32 {
251+ return ErrInvalidHello
252+ }
253+ }
254+ if h.PreferredTabID != "" {
255+ if h.Intent != IntentAttach && h.Intent != IntentResume {
256+ return ErrInvalidHello
257+ }
258+ if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
259+ return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
260+ }
261+ }
262+ if h.ExactTarget != nil {
263+ if h.Intent != IntentAttach && h.Intent != IntentResume {
264+ return ErrInvalidHello
265+ }
266+ if err := h.ExactTarget.Validate(); err != nil {
267+ return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
268+ }
269+ if h.Name != h.ExactTarget.SessionName {
270+ return ErrInvalidHello
271+ }
272+ if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
273+ return ErrInvalidHello
274+ }
275+ }
276+ if h.RemoteTarget == nil {
277+ return nil
278+ }
279+ if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
280+ return ErrInvalidHello
281+ }
282+ if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
283+ return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
284+ }
285+ if h.Name != h.RemoteTarget.SessionName {
286+ return ErrInvalidHello
287+ }
288+ return nil
289+}
290+
291+func ValidateAttachTarget(m AttachTarget) error {
292+ if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
293+ return ErrInvalidAttachTarget
294+ }
295+ if err := domain.ValidateSessionName(m.Session); err != nil {
296+ return ErrInvalidAttachTarget
297+ }
298+ if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
299+ return ErrInvalidAttachTarget
300+ }
301+ if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
302+ return ErrInvalidAttachTarget
303+ }
304+ if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
305+ return ErrInvalidAttachTarget
306+ }
307+ if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
308+ return ErrInvalidAttachTarget
309+ }
310+ if m.RemoteTarget == nil {
311+ return nil
312+ }
313+ if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
314+ return ErrInvalidAttachTarget
315+ }
316+ if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
317+ return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
318+ }
319+ if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
320+ return ErrInvalidAttachTarget
321+ }
322+ return nil
323+}
M internal/ports/wire.go +125 -601
@@ -18,6 +18,7 @@ import (
1818
1919 renderer "github.com/bnema/vev-vt"
2020 "github.com/bnema/vev/internal/domain"
21+ "github.com/bnema/vev/internal/protocol"
2122 )
2223
2324 // errShortPayload is returned when a payload ends before a required field
@@ -53,338 +54,6 @@ var outputCompressorPool = sync.Pool{New: func() any {
5354 return writer
5455 }}
5556
56-// ErrInvalidSize reports a terminal size that cannot be represented by the
57-// protocol or would exceed the bounded screen area.
58-var ErrInvalidSize = errors.New("ports: invalid terminal size")
59-
60-// ErrInvalidHello reports a malformed Hello semantic value.
61-var ErrInvalidHello = errors.New("ports: invalid hello")
62-
63-// ErrInvalidOutput reports a malformed Output semantic value.
64-var ErrInvalidOutput = errors.New("ports: invalid output")
65-
66-// ErrInvalidAck reports a malformed Ack semantic value.
67-var ErrInvalidAck = errors.New("ports: invalid ack")
68-
69-// ErrInvalidAttachTarget reports a malformed attach target.
70-var ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
71-
72-// ErrInvalidNavigation reports an unknown or impossible navigation value.
73-var ErrInvalidNavigation = errors.New("ports: invalid navigation")
74-
75-// Intent values carried by Hello, describing what the client wants to do.
76-const (
77- IntentEphemeral uint8 = 0
78- IntentNew uint8 = 1
79- IntentAttach uint8 = 2
80- IntentResume uint8 = 3
81-)
82-
83-// EnvironmentPolicy controls which side owns the environment used for future
84-// PTY children. Direct CLI attaches keep the historical client-owned policy;
85-// picker handoffs use daemon ownership so a local environment cannot leak into
86-// a remote daemon.
87-type EnvironmentPolicy uint8
88-
89-const (
90- EnvironmentPolicyClientOwned EnvironmentPolicy = iota
91- EnvironmentPolicyDaemonOwned
92-)
93-
94-func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
95- return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
96-}
97-
98-func validNavigationCapabilities(capabilities NavigationCapabilities) bool {
99- return capabilities&^(NavigationCapabilityHomePicker|NavigationCapabilityBack) == 0
100-}
101-
102-func validStartupOverlay(overlay StartupOverlay) bool {
103- return overlay == StartupOverlayNone || overlay == StartupOverlaySessionPicker
104-}
105-
106-// ValidateNavigation validates route capabilities independently of the rest of
107-// the Hello payload. homePickerRoute indicates that the route may open its
108-// home/session picker (for example, a remote target or daemon-owned route).
109-func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOverlay, homePickerRoute bool) error {
110- if !validNavigationCapabilities(capabilities) || !validStartupOverlay(overlay) {
111- return ErrInvalidNavigation
112- }
113- home := capabilities&NavigationCapabilityHomePicker != 0
114- back := capabilities&NavigationCapabilityBack != 0
115- if (home && !homePickerRoute) || back != (overlay == StartupOverlaySessionPicker) {
116- return ErrInvalidNavigation
117- }
118- if homePickerRoute && back {
119- return ErrInvalidNavigation
120- }
121- return nil
122-}
123-
124-func validateHelloNavigation(h Hello) error {
125- if h.Intent != IntentAttach && h.Intent != IntentResume {
126- if h.NavigationCapabilities != 0 || h.StartupOverlay != StartupOverlayNone {
127- return ErrInvalidNavigation
128- }
129- return nil
130- }
131- return ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
132-}
133-
134-// Capability bits advertised in Welcome.
135-const (
136- CapabilityResume uint32 = 1 << 0
137- CapabilityUDP uint32 = 1 << 1
138- CapabilityPredict uint32 = 1 << 2
139-)
140-
141-// ErrorMsg codes.
142-const (
143- ErrVersionMismatch uint16 = 1
144- ErrNoSuchSession uint16 = 2
145- ErrNameTaken uint16 = 3
146- ErrServerShutdown uint16 = 4
147- ErrInvalidSessionName uint16 = 5
148- ErrUnknownCommand uint16 = 6
149- ErrNotScriptable uint16 = 7
150- ErrInvalidCommandArgs uint16 = 8
151- ErrNoSuchTarget uint16 = 9
152- ErrAmbiguousTarget uint16 = 10
153- ErrInternal uint16 = 255
154-)
155-
156-// Detached reasons.
157-const (
158- ReasonDetach uint8 = 0
159- ReasonSessionKilled uint8 = 1
160- ReasonServerShutdown uint8 = 2
161- ReasonReplaced uint8 = 3
162-)
163-
164-// Hello is sent by the client immediately after connecting.
165-type Hello struct {
166- Version uint16
167- Intent uint8
168- ClientID [16]byte
169- ResumeToken uint64
170- Name string
171- Size domain.Size
172- PixelWidth int
173- PixelHeight int
174- TermEnv string
175- Cwd string
176- TrueColor bool
177- // KittyDirectGraphics is an explicit declaration that the client has
178- // probed its direct outer terminal and it accepts Kitty graphics output.
179- // Environment values are never sufficient to set this capability.
180- KittyDirectGraphics bool
181- // MaxOutputInFlight is the requested maximum number of unacknowledged
182- // state-bearing output frames.
183- MaxOutputInFlight uint8
184- // Env is the complete exported client environment for future PTY children.
185- // Entries are opaque strings so their ordering and contents are preserved.
186- Env []string
187- // RemoteTarget is present only for a picker-selected remote attach. It is
188- // carried through reconnects and is validated by the owning daemon before
189- // it creates or publishes any session resources.
190- RemoteTarget *domain.RemoteSessionTarget
191- // ExactTarget is an optional lifecycle/name identity selected by the client
192- // ledger. It is transport-neutral and never replaces the daemon's authority
193- // to validate or commit the target.
194- ExactTarget *ExactSessionTarget
195- // PreferredTabID is a client-owned route cursor. The daemon treats it as an
196- // attachment-local hint and falls back when the stable tab no longer exists.
197- PreferredTabID domain.TabStableID
198- EnvironmentPolicy EnvironmentPolicy
199- NavigationCapabilities NavigationCapabilities
200- StartupOverlay StartupOverlay
201- // Remote identifies a direct remote carriage even when no exact picker
202- // target is present. It is daemon-facing so remote rendering backends can
203- // be disabled consistently for both direct and picker attaches.
204- Remote bool
205-}
206-
207-// Input carries raw bytes typed/pasted by the client, destined for the PTY.
208-type Input struct {
209- InputSeq uint64
210- Data []byte
211-}
212-
213-// Geometry returns the complete controlling-terminal geometry carried by the
214-// handshake. Pixel dimensions are optional as a pair.
215-func (h Hello) Geometry() domain.Geometry {
216- return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
217-}
218-
219-// Resize notifies the daemon of a client-side terminal geometry change.
220-type Resize struct {
221- Size domain.Size
222- PixelWidth int
223- PixelHeight int
224-}
225-
226-// Geometry returns the complete controlling-terminal geometry carried by the
227-// resize message. Pixel dimensions are optional as a pair.
228-func (m Resize) Geometry() domain.Geometry {
229- return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels()
230-}
231-
232-// Theme reports the client's terminal foreground/background colors, ANSI
233-// palette entries, and whether the client terminal supports truecolor.
234-type Theme struct {
235- HasForeground bool
236- Foreground renderer.RGB
237- HasBackground bool
238- Background renderer.RGB
239- TrueColor bool
240- SchemeKnown bool
241- Light bool
242- PaletteKnown uint16
243- Palette [16]renderer.RGB
244-}
245-
246-// ImagePush carries a clipboard image from a remote client, to be written to
247-// a temp file and injected into the focused pane's PTY as a path (see
248-// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
249-type ImagePush struct {
250- InputSeq uint64
251- Mime string
252- Data []byte
253-}
254-
255-// ClientNotice reports a fixed client-side event for daemon-rendered user
256-// feedback. Action values are deliberately closed so a client cannot inject
257-// arbitrary display text into a shared session.
258-type ClientNotice struct {
259- Action uint8
260-}
261-
262-const (
263- ClientNoticeClipboardFallback uint8 = 1
264- ClientNoticeClipboardTooLarge uint8 = 2
265- ClientNoticeLinkDegraded uint8 = 3
266- ClientNoticeLinkConnected uint8 = 4
267-)
268-
269-func validClientNoticeAction(action uint8) bool {
270- switch action {
271- case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
272- return true
273- default:
274- return false
275- }
276-}
277-
278-func validDetachedReason(reason uint8) bool {
279- switch reason {
280- case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
281- return true
282- default:
283- return false
284- }
285-}
286-
287-// Detach asks the daemon to detach the current client without killing the
288-// session.
289-type Detach struct{}
290-
291-// Ping is a keepalive/liveness probe sent by either side.
292-type Ping struct{}
293-
294-// Pong answers a Ping.
295-type Pong struct{}
296-
297-// Ack acknowledges receipt/application of an output state in one output epoch.
298-type Ack struct {
299- Epoch uint64
300- State uint64
301-}
302-
303-// Welcome is the daemon's reply to a successful Hello.
304-type Welcome struct {
305- SessionID string
306- SessionName string
307- Ephemeral bool
308- ResumeToken uint64
309- Capabilities uint32
310- CommittedIdentity *CommittedRouteIdentity
311-}
312-
313-// ErrorMsg reports a protocol- or session-level failure to the client.
314-type ErrorMsg struct {
315- Code uint16
316- Text string
317-}
318-
319-// Output carries one ordered output publication for an attachment.
320-type Output struct {
321- Epoch uint64
322- Base uint64
323- New uint64
324- Echo uint64
325- ViewRevision uint64
326- Size domain.Size
327- Full bool
328- Data []byte
329-}
330-
331-// AttachTarget identifies the endpoint and exact session/tab selected for a
332-// new attachment. Endpoint and Session remain the compatibility route fields;
333-// RemoteTarget is the authoritative picker identity when non-nil.
334-type AttachTarget struct {
335- Endpoint string
336- Session string
337- Intent uint8
338- RemoteTarget *domain.RemoteSessionTarget
339- ExactTarget *ExactSessionTarget
340- EnvironmentPolicy EnvironmentPolicy
341- SamePeer bool
342-}
343-
344-// Detached tells a client it has been disconnected from its session and why.
345-type Detached struct {
346- Reason uint8
347-}
348-
349-// List asks the daemon to enumerate its live sessions. It carries no
350-// fields; the request is fully described by its message type.
351-type List struct{}
352-
353-// Kill asks the daemon to terminate one named session or all sessions.
354-type Kill struct {
355- Name string
356- All bool
357-}
358-
359-// SessionState is the client-visible lifecycle state of a session.
360-type SessionState uint8
361-
362-const (
363- SessionUp SessionState = iota
364- SessionDown
365- SessionBroken
366-)
367-
368-// SessionInfo describes one session in a Sessions listing.
369-type SessionInfo struct {
370- SessionID string
371- Name string
372- State SessionState
373- Ephemeral bool
374- Tabs uint16
375- Attached bool
376-}
377-
378-// Sessions is the daemon's reply to a List, enumerating live sessions.
379-type Sessions struct {
380- Sessions []SessionInfo
381-}
382-
383-// OutputResetRequest asks a remote output stream to rebase.
384-type OutputResetRequest struct{}
385-
386-// SessionTabMeta describes one tab in a remote session's metadata snapshot.
387-// ID is stable for the lifetime of its remote tab and ordered only for display.
38857 // payloadWriter builds a message payload by appending fields in wire order.
38958 type payloadWriter struct {
39059 b []byte
@@ -521,7 +190,7 @@ func skipRemoteTargetSection(r *payloadReader) error {
521190 if err != nil {
522191 return err
523192 }
524- if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {
193+ if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
525194 return errInvalidEnum
526195 }
527196 return nil
@@ -596,7 +265,7 @@ func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget
596265 if err != nil {
597266 return nil, 0, err
598267 }
599- if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {
268+ if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
600269 return nil, 0, errInvalidEnum
601270 }
602271 return target, EnvironmentPolicy(policy), nil
@@ -753,141 +422,14 @@ func PeekHelloVersion(b []byte) (uint16, bool) {
753422
754423 // ValidateHello validates Hello semantics without retaining or allocating any
755424 // payload data.
756-func ValidateHello(h Hello) error {
757- if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
758- return ErrInvalidHello
759- }
760- if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
761- return fmt.Errorf("%w: geometry", ErrInvalidHello)
762- }
763- if !validEnvironmentPolicy(h.EnvironmentPolicy) {
764- return ErrInvalidHello
765- }
766- if err := validateHelloNavigation(h); err != nil {
767- return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
768- }
769- if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
770- return ErrInvalidHello
771- }
772- for _, entry := range h.Env {
773- if uint64(len(entry)) > math.MaxUint32 {
774- return ErrInvalidHello
775- }
776- }
777- if h.PreferredTabID != "" {
778- if h.Intent != IntentAttach && h.Intent != IntentResume {
779- return ErrInvalidHello
780- }
781- if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
782- return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
783- }
784- }
785- if h.ExactTarget != nil {
786- if h.Intent != IntentAttach && h.Intent != IntentResume {
787- return ErrInvalidHello
788- }
789- if err := h.ExactTarget.Validate(); err != nil {
790- return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
791- }
792- if h.Name != h.ExactTarget.SessionName {
793- return ErrInvalidHello
794- }
795- if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
796- return ErrInvalidHello
797- }
798- }
799- if h.RemoteTarget == nil {
800- if h.EnvironmentPolicy != EnvironmentPolicyClientOwned && h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
801- return ErrInvalidHello
802- }
803- return nil
804- }
805- if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
806- return ErrInvalidHello
807- }
808- if h.Intent != IntentAttach && h.Intent != IntentResume {
809- return ErrInvalidHello
810- }
811- if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
812- return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
813- }
814- if h.Name != h.RemoteTarget.SessionName {
815- return ErrInvalidHello
816- }
817- return nil
818-}
819425
820426 // ValidateOutput validates the final output state before data allocation.
821-func ValidateOutput(m Output) error {
822- if m.Epoch == 0 {
823- return ErrInvalidOutput
824- }
825- if m.New == 0 {
826- if m.Base != 0 || m.Full {
827- return ErrInvalidOutput
828- }
829- } else if (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
830- return ErrInvalidOutput
831- }
832- if err := ValidateSize(m.Size); err != nil {
833- return fmt.Errorf("%w: size", ErrInvalidOutput)
834- }
835- if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxOutputDataLen {
836- return ErrInvalidOutput
837- }
838- return nil
839-}
840427
841428 // ValidateAck validates an epoch-scoped output acknowledgement.
842-func ValidateAck(m Ack) error {
843- if m.Epoch == 0 {
844- return ErrInvalidAck
845- }
846- return nil
847-}
848429
849430 // ValidateAttachTarget validates a client-owned route handoff. An empty
850431 // Endpoint selects another session on the currently connected daemon; a
851432 // non-empty Endpoint selects a discovered remote daemon.
852-func ValidateAttachTarget(m AttachTarget) error {
853- if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
854- return ErrInvalidAttachTarget
855- }
856- if err := domain.ValidateSessionName(m.Session); err != nil {
857- return ErrInvalidAttachTarget
858- }
859- if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
860- return ErrInvalidAttachTarget
861- }
862- if m.Intent == IntentResume {
863- return ErrInvalidAttachTarget
864- }
865- if !validEnvironmentPolicy(m.EnvironmentPolicy) {
866- return ErrInvalidAttachTarget
867- }
868- if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
869- return ErrInvalidAttachTarget
870- }
871- if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
872- return ErrInvalidAttachTarget
873- }
874- if m.RemoteTarget == nil {
875- return nil
876- }
877- if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
878- return ErrInvalidAttachTarget
879- }
880- if m.Intent != IntentAttach {
881- return ErrInvalidAttachTarget
882- }
883- if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
884- return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
885- }
886- if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
887- return ErrInvalidAttachTarget
888- }
889- return nil
890-}
891433
892434 func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {
893435 if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
@@ -1105,12 +647,12 @@ func UnmarshalHello(b []byte) (Hello, error) {
1105647 if err != nil {
1106648 return Hello{}, err
1107649 }
1108- h.NavigationCapabilities = NavigationCapabilities(capabilities)
650+ h.NavigationCapabilities = protocol.NavigationCapabilities(capabilities)
1109651 overlay, err := r.getUint8()
1110652 if err != nil {
1111653 return Hello{}, err
1112654 }
1113- h.StartupOverlay = StartupOverlay(overlay)
655+ h.StartupOverlay = protocol.StartupOverlay(overlay)
1114656 if h.Remote, err = r.getBool(); err != nil {
1115657 return Hello{}, err
1116658 }
@@ -1146,7 +688,7 @@ func UnmarshalInput(b []byte) (Input, error) {
1146688 }
1147689
1148690 // MarshalImagePush encodes m into an ImagePush message payload.
1149-func MarshalImagePush(m ImagePush) []byte {
691+func MarshalImagePush(m protocol.ImagePush) []byte {
1150692 w := payloadWriter{}
1151693 w.putUint64(m.InputSeq)
1152694 w.putString(m.Mime)
@@ -1157,17 +699,17 @@ func MarshalImagePush(m ImagePush) []byte {
1157699 // UnmarshalImagePush decodes an ImagePush message payload. After the fixed
1158700 // input sequence and length-prefixed mime string, the rest of the payload is
1159701 // data; there is no length prefix for it.
1160-func UnmarshalImagePush(b []byte) (ImagePush, error) {
702+func UnmarshalImagePush(b []byte) (protocol.ImagePush, error) {
1161703 r := payloadReader{b: b}
1162704 seq, err := r.getUint64()
1163705 if err != nil {
1164- return ImagePush{}, err
706+ return protocol.ImagePush{}, err
1165707 }
1166708 mime, err := r.getString()
1167709 if err != nil {
1168- return ImagePush{}, err
710+ return protocol.ImagePush{}, err
1169711 }
1170- return ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil
712+ return protocol.ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil
1171713 }
1172714
1173715 // MarshalClientNotice encodes a fixed one-byte client notice action.
@@ -1183,7 +725,7 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
1183725 if err != nil {
1184726 return ClientNotice{}, err
1185727 }
1186- if !validClientNoticeAction(action) {
728+ if ValidateClientNotice(ClientNotice{Action: action}) != nil {
1187729 return ClientNotice{}, errors.New("ports: unknown client notice action")
1188730 }
1189731 if err := r.done(); err != nil {
@@ -1193,8 +735,8 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
1193735 }
1194736
1195737 // MarshalResize encodes m into a Resize message payload.
1196-func MarshalResize(m Resize) ([]byte, error) {
1197- if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
738+func MarshalResize(m protocol.Resize) ([]byte, error) {
739+ if err := protocol.ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
1198740 return nil, err
1199741 }
1200742 w := payloadWriter{}
@@ -1206,7 +748,7 @@ func MarshalResize(m Resize) ([]byte, error) {
1206748 }
1207749
1208750 // MarshalTheme encodes m into a 57-byte fixed-width Theme message payload.
1209-func MarshalTheme(m Theme) []byte {
751+func MarshalTheme(m protocol.Theme) []byte {
1210752 var flags uint8
1211753 if m.HasForeground {
1212754 flags |= 0x01
@@ -1242,45 +784,45 @@ func MarshalTheme(m Theme) []byte {
1242784 }
1243785
1244786 // UnmarshalTheme decodes a 57-byte fixed-width Theme message payload.
1245-func UnmarshalTheme(b []byte) (Theme, error) {
787+func UnmarshalTheme(b []byte) (protocol.Theme, error) {
1246788 r := payloadReader{b: b}
1247789 flags, err := r.getUint8()
1248790 if err != nil {
1249- return Theme{}, err
791+ return protocol.Theme{}, err
1250792 }
1251793 if flags&^uint8(0x1f) != 0 {
1252- return Theme{}, errInvalidEnum
794+ return protocol.Theme{}, errInvalidEnum
1253795 }
1254796 fgR, err := r.getUint8()
1255797 if err != nil {
1256- return Theme{}, err
798+ return protocol.Theme{}, err
1257799 }
1258800 fgG, err := r.getUint8()
1259801 if err != nil {
1260- return Theme{}, err
802+ return protocol.Theme{}, err
1261803 }
1262804 fgB, err := r.getUint8()
1263805 if err != nil {
1264- return Theme{}, err
806+ return protocol.Theme{}, err
1265807 }
1266808 bgR, err := r.getUint8()
1267809 if err != nil {
1268- return Theme{}, err
810+ return protocol.Theme{}, err
1269811 }
1270812 bgG, err := r.getUint8()
1271813 if err != nil {
1272- return Theme{}, err
814+ return protocol.Theme{}, err
1273815 }
1274816 bgB, err := r.getUint8()
1275817 if err != nil {
1276- return Theme{}, err
818+ return protocol.Theme{}, err
1277819 }
1278820 paletteKnown, err := r.getUint16()
1279821 if err != nil {
1280- return Theme{}, err
822+ return protocol.Theme{}, err
1281823 }
1282824
1283- m := Theme{
825+ m := protocol.Theme{
1284826 HasForeground: flags&0x01 != 0,
1285827 Foreground: renderer.RGB{R: fgR, G: fgG, B: fgB},
1286828 HasBackground: flags&0x02 != 0,
@@ -1292,50 +834,50 @@ func UnmarshalTheme(b []byte) (Theme, error) {
1292834 }
1293835 for i := range m.Palette {
1294836 if m.Palette[i].R, err = r.getUint8(); err != nil {
1295- return Theme{}, err
837+ return protocol.Theme{}, err
1296838 }
1297839 if m.Palette[i].G, err = r.getUint8(); err != nil {
1298- return Theme{}, err
840+ return protocol.Theme{}, err
1299841 }
1300842 if m.Palette[i].B, err = r.getUint8(); err != nil {
1301- return Theme{}, err
843+ return protocol.Theme{}, err
1302844 }
1303845 }
1304846 if err := r.done(); err != nil {
1305- return Theme{}, err
847+ return protocol.Theme{}, err
1306848 }
1307849 return m, nil
1308850 }
1309851
1310852 // UnmarshalResize decodes a Resize message payload.
1311-func UnmarshalResize(b []byte) (Resize, error) {
853+func UnmarshalResize(b []byte) (protocol.Resize, error) {
1312854 r := payloadReader{b: b}
1313855 cols, err := r.getUint16()
1314856 if err != nil {
1315- return Resize{}, err
857+ return protocol.Resize{}, err
1316858 }
1317859 rows, err := r.getUint16()
1318860 if err != nil {
1319- return Resize{}, err
861+ return protocol.Resize{}, err
1320862 }
1321863 pixelWidth, err := r.getUint16()
1322864 if err != nil {
1323- return Resize{}, err
865+ return protocol.Resize{}, err
1324866 }
1325867 pixelHeight, err := r.getUint16()
1326868 if err != nil {
1327- return Resize{}, err
869+ return protocol.Resize{}, err
1328870 }
1329871 if err := r.done(); err != nil {
1330- return Resize{}, err
872+ return protocol.Resize{}, err
1331873 }
1332- m := Resize{
874+ m := protocol.Resize{
1333875 Size: domain.Size{Cols: int(cols), Rows: int(rows)},
1334876 PixelWidth: int(pixelWidth),
1335877 PixelHeight: int(pixelHeight),
1336878 }
1337- if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
1338- return Resize{}, err
879+ if err := protocol.ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
880+ return protocol.Resize{}, err
1339881 }
1340882 return m, nil
1341883 }
@@ -1383,8 +925,8 @@ func UnmarshalPong(b []byte) (Pong, error) {
1383925 }
1384926
1385927 // MarshalAck encodes m into an epoch/state Ack payload.
1386-func MarshalAck(m Ack) ([]byte, error) {
1387- if err := ValidateAck(m); err != nil {
928+func MarshalAck(m protocol.Ack) ([]byte, error) {
929+ if err := protocol.ValidateAck(m); err != nil {
1388930 return nil, err
1389931 }
1390932 w := payloadWriter{}
@@ -1394,21 +936,21 @@ func MarshalAck(m Ack) ([]byte, error) {
1394936 }
1395937
1396938 // UnmarshalAck decodes an epoch/state Ack payload.
1397-func UnmarshalAck(b []byte) (Ack, error) {
939+func UnmarshalAck(b []byte) (protocol.Ack, error) {
1398940 r := payloadReader{b: b}
1399- var m Ack
941+ var m protocol.Ack
1400942 var err error
1401943 if m.Epoch, err = r.getUint64(); err != nil {
1402- return Ack{}, err
944+ return protocol.Ack{}, err
1403945 }
1404946 if m.State, err = r.getUint64(); err != nil {
1405- return Ack{}, err
947+ return protocol.Ack{}, err
1406948 }
1407949 if err := r.done(); err != nil {
1408- return Ack{}, err
950+ return protocol.Ack{}, err
1409951 }
1410- if err := ValidateAck(m); err != nil {
1411- return Ack{}, err
952+ if err := protocol.ValidateAck(m); err != nil {
953+ return protocol.Ack{}, err
1412954 }
1413955 return m, nil
1414956 }
@@ -1461,7 +1003,7 @@ func UnmarshalWelcome(b []byte) (Welcome, error) {
14611003 return Welcome{}, err
14621004 }
14631005 if m.CommittedIdentity != nil && (m.CommittedIdentity.Target.SessionName != m.SessionName || m.CommittedIdentity.Ephemeral != m.Ephemeral) {
1464- return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", ErrInvalidRouteWire)
1006+ return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)
14651007 }
14661008 if err := r.done(); err != nil {
14671009 return Welcome{}, err
@@ -1498,8 +1040,8 @@ func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {
14981040 // MarshalOutput encodes m into the epoch/base/new/echo/viewRevision/
14991041 // size/full/compression/decoded-length/data message layout. Compression is
15001042 // limited to large, full snapshots and is retained only when it saves bytes.
1501-func MarshalOutput(m Output) ([]byte, error) {
1502- if err := ValidateOutput(m); err != nil {
1043+func MarshalOutput(m protocol.Output) ([]byte, error) {
1044+ if err := protocol.ValidateOutput(m); err != nil {
15031045 return nil, err
15041046 }
15051047 kind, data, err := compressOutputData(m)
@@ -1521,7 +1063,7 @@ func MarshalOutput(m Output) ([]byte, error) {
15211063 return w.b, nil
15221064 }
15231065
1524-func compressOutputData(m Output) (byte, []byte, error) {
1066+func compressOutputData(m protocol.Output) (byte, []byte, error) {
15251067 if !m.Full || len(m.Data) < outputCompressionThreshold {
15261068 return outputCompressionNone, m.Data, nil
15271069 }
@@ -1546,83 +1088,83 @@ func compressOutputData(m Output) (byte, []byte, error) {
15461088
15471089 // UnmarshalOutput strictly decodes one final Output payload. Header semantics
15481090 // are validated before its bounded decoded data allocation.
1549-func UnmarshalOutput(b []byte) (Output, error) {
1091+func UnmarshalOutput(b []byte) (protocol.Output, error) {
15501092 if len(b) > MaxFrameLen-1 {
1551- return Output{}, ErrInvalidOutput
1093+ return protocol.Output{}, protocol.ErrInvalidOutput
15521094 }
15531095 if len(b) < outputPayloadOverhead {
1554- return Output{}, errShortPayload
1096+ return protocol.Output{}, errShortPayload
15551097 }
15561098 r := payloadReader{b: b}
1557- var m Output
1099+ var m protocol.Output
15581100 var err error
15591101 if m.Epoch, err = r.getUint64(); err != nil {
1560- return Output{}, err
1102+ return protocol.Output{}, err
15611103 }
15621104 if m.Base, err = r.getUint64(); err != nil {
1563- return Output{}, err
1105+ return protocol.Output{}, err
15641106 }
15651107 if m.New, err = r.getUint64(); err != nil {
1566- return Output{}, err
1108+ return protocol.Output{}, err
15671109 }
15681110 if m.Echo, err = r.getUint64(); err != nil {
1569- return Output{}, err
1111+ return protocol.Output{}, err
15701112 }
15711113 if m.ViewRevision, err = r.getUint64(); err != nil {
1572- return Output{}, err
1114+ return protocol.Output{}, err
15731115 }
15741116 cols, err := r.getUint16()
15751117 if err != nil {
1576- return Output{}, err
1118+ return protocol.Output{}, err
15771119 }
15781120 rows, err := r.getUint16()
15791121 if err != nil {
1580- return Output{}, err
1122+ return protocol.Output{}, err
15811123 }
15821124 m.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
15831125 if m.Full, err = r.getBool(); err != nil {
1584- return Output{}, err
1126+ return protocol.Output{}, err
15851127 }
1586- if err := ValidateOutput(m); err != nil {
1587- return Output{}, err
1128+ if err := protocol.ValidateOutput(m); err != nil {
1129+ return protocol.Output{}, err
15881130 }
15891131 kind, err := r.getUint8()
15901132 if err != nil {
1591- return Output{}, err
1133+ return protocol.Output{}, err
15921134 }
15931135 decodedLen, err := r.getUint32()
15941136 if err != nil {
1595- return Output{}, err
1137+ return protocol.Output{}, err
15961138 }
1597- if int64(decodedLen) > int64(MaxOutputDataLen) {
1598- return Output{}, ErrInvalidOutput
1139+ if int64(decodedLen) > int64(protocol.MaxOutputDataLen) {
1140+ return protocol.Output{}, protocol.ErrInvalidOutput
15991141 }
16001142 data, err := r.getLongBytes()
16011143 if err != nil {
1602- return Output{}, err
1144+ return protocol.Output{}, err
16031145 }
16041146 if err := r.done(); err != nil {
1605- return Output{}, err
1147+ return protocol.Output{}, err
16061148 }
16071149 switch kind {
16081150 case outputCompressionNone:
16091151 if uint32(len(data)) != decodedLen {
1610- return Output{}, ErrInvalidOutput
1152+ return protocol.Output{}, protocol.ErrInvalidOutput
16111153 }
16121154 m.Data = data
16131155 case outputCompressionZlib:
16141156 if !m.Full {
1615- return Output{}, ErrInvalidOutput
1157+ return protocol.Output{}, protocol.ErrInvalidOutput
16161158 }
16171159 m.Data, err = decompressOutputData(data, int(decodedLen))
16181160 if err != nil {
1619- return Output{}, ErrInvalidOutput
1161+ return protocol.Output{}, protocol.ErrInvalidOutput
16201162 }
16211163 default:
1622- return Output{}, errInvalidEnum
1164+ return protocol.Output{}, errInvalidEnum
16231165 }
1624- if err := ValidateOutput(m); err != nil {
1625- return Output{}, err
1166+ if err := protocol.ValidateOutput(m); err != nil {
1167+ return protocol.Output{}, err
16261168 }
16271169 return m, nil
16281170 }
@@ -1742,7 +1284,7 @@ func UnmarshalDetached(b []byte) (Detached, error) {
17421284 if err != nil {
17431285 return Detached{}, err
17441286 }
1745- if !validDetachedReason(reason) {
1287+ if ValidateDetached(Detached{Reason: reason}) != nil {
17461288 return Detached{}, errInvalidEnum
17471289 }
17481290 if err := r.done(); err != nil {
@@ -1948,7 +1490,7 @@ func getPreviewStyle(r *payloadReader) (renderer.Style, error) {
19481490 return renderer.Style{}, err
19491491 }
19501492 if flags&0x80 != 0 {
1951- return renderer.Style{}, ErrRemotePreviewUnsupportedStyle
1493+ return renderer.Style{}, protocol.ErrRemotePreviewUnsupportedStyle
19521494 }
19531495 attrs, err := r.getUint16()
19541496 if err != nil {
@@ -1989,8 +1531,8 @@ func getPreviewStyle(r *payloadReader) (renderer.Style, error) {
19891531 ForegroundRGB: fgrgb, BackgroundRGB: bgrgb, UnderlineColorRGB: ulrgb}, nil
19901532 }
19911533
1992-func MarshalRemotePreviewRequest(request RemotePreviewRequest) []byte {
1993- if ValidateRemotePreviewRequest(request) != nil {
1534+func MarshalRemotePreviewRequest(request protocol.RemotePreviewRequest) []byte {
1535+ if protocol.ValidateRemotePreviewRequest(request) != nil {
19941536 return nil
19951537 }
19961538 w := payloadWriter{}
@@ -2005,12 +1547,12 @@ func MarshalRemotePreviewRequest(request RemotePreviewRequest) []byte {
20051547 return w.b
20061548 }
20071549
2008-func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
2009- if len(data) > RemotePreviewMaxBytes {
2010- return RemotePreviewRequest{}, ErrInvalidRemotePreviewRequest
1550+func UnmarshalRemotePreviewRequest(data []byte) (protocol.RemotePreviewRequest, error) {
1551+ if len(data) > protocol.RemotePreviewMaxBytes {
1552+ return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
20111553 }
20121554 r := payloadReader{b: data}
2013- var q RemotePreviewRequest
1555+ var q protocol.RemotePreviewRequest
20141556 var err error
20151557 if q.Version, err = r.getUint16(); err != nil {
20161558 return q, err
@@ -2043,7 +1585,7 @@ func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
20431585 if err := r.done(); err != nil {
20441586 return q, err
20451587 }
2046- if err := ValidateRemotePreviewRequest(q); err != nil {
1588+ if err := protocol.ValidateRemotePreviewRequest(q); err != nil {
20471589 return q, err
20481590 }
20491591 return q, nil
@@ -2051,8 +1593,8 @@ func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
20511593
20521594 const previewCellWireSize = 4 + 1 + 1 + 2 + 2 + 2 + 1 + 2 + 3 + 3 + 3
20531595
2054-func MarshalRemotePreview(preview RemotePreview) []byte {
2055- if ValidateRemotePreview(preview) != nil {
1596+func MarshalRemotePreview(preview protocol.RemotePreview) []byte {
1597+ if protocol.ValidateRemotePreview(preview) != nil {
20561598 return nil
20571599 }
20581600 w := payloadWriter{}
@@ -2076,12 +1618,12 @@ func MarshalRemotePreview(preview RemotePreview) []byte {
20761618 return w.b
20771619 }
20781620
2079-func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
2080- if len(data) > RemotePreviewMaxBytes {
2081- return RemotePreview{}, ErrRemotePreviewTooLarge
1621+func UnmarshalRemotePreview(data []byte) (protocol.RemotePreview, error) {
1622+ if len(data) > protocol.RemotePreviewMaxBytes {
1623+ return protocol.RemotePreview{}, protocol.ErrRemotePreviewTooLarge
20821624 }
20831625 r := payloadReader{b: data}
2084- var p RemotePreview
1626+ var p protocol.RemotePreview
20851627 var err error
20861628 if p.Version, err = r.getUint16(); err != nil {
20871629 return p, err
@@ -2090,7 +1632,7 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
20901632 if err != nil {
20911633 return p, err
20921634 }
2093- p.Status = RemotePreviewStatus(status)
1635+ p.Status = protocol.RemotePreviewStatus(status)
20941636 id, err := r.getBytes(16)
20951637 if err != nil {
20961638 return p, err
@@ -2114,8 +1656,8 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
21141656 if err != nil {
21151657 return p, err
21161658 }
2117- if count > RemotePreviewMaxCells || uint64(count) > uint64(len(r.b)/previewCellWireSize) {
2118- return p, ErrRemotePreviewTooLarge
1659+ if count > protocol.RemotePreviewMaxCells || uint64(count) > uint64(len(r.b)/previewCellWireSize) {
1660+ return p, protocol.ErrRemotePreviewTooLarge
21191661 }
21201662 if count != 0 {
21211663 p.Cells = make([]renderer.Cell, 0, int(count))
@@ -2129,7 +1671,7 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
21291671 return p, e
21301672 }
21311673 if flags&^uint8(1) != 0 {
2132- return p, ErrInvalidRemotePreview
1674+ return p, protocol.ErrInvalidRemotePreview
21331675 }
21341676 style, e := getPreviewStyle(&r)
21351677 if e != nil {
@@ -2141,18 +1683,18 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
21411683 if err := r.done(); err != nil {
21421684 return p, err
21431685 }
2144- if err := ValidateRemotePreview(p); err != nil {
1686+ if err := protocol.ValidateRemotePreview(p); err != nil {
21451687 return p, err
21461688 }
21471689 return p, nil
21481690 }
21491691
21501692 // MarshalNavigationDirective encodes one bounded navigation directive.
2151-func MarshalNavigationDirective(directive NavigationDirective) []byte {
2152- if directive.Action != NavigationOpenHomePicker && directive.Action != NavigationBack {
1693+func MarshalNavigationDirective(directive protocol.NavigationDirective) []byte {
1694+ if directive.Action != protocol.NavigationOpenHomePicker && directive.Action != protocol.NavigationBack {
21531695 return nil
21541696 }
2155- if directive.Action == NavigationOpenHomePicker && directive.LeaseID.IsZero() || directive.Action == NavigationBack && !directive.LeaseID.IsZero() {
1697+ if directive.Action == protocol.NavigationOpenHomePicker && directive.LeaseID.IsZero() || directive.Action == protocol.NavigationBack && !directive.LeaseID.IsZero() {
21561698 return nil
21571699 }
21581700 w := payloadWriter{}
@@ -2162,51 +1704,33 @@ func MarshalNavigationDirective(directive NavigationDirective) []byte {
21621704 }
21631705
21641706 // UnmarshalNavigationDirective decodes one strict navigation directive.
2165-func UnmarshalNavigationDirective(b []byte) (NavigationDirective, error) {
1707+func UnmarshalNavigationDirective(b []byte) (protocol.NavigationDirective, error) {
21661708 r := payloadReader{b: b}
21671709 value, err := r.getUint8()
21681710 if err != nil {
2169- return NavigationDirective{}, ErrInvalidNavigation
1711+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
21701712 }
2171- lease, err := r.getBytes(len(ParkedRouteLeaseID{}))
1713+ lease, err := r.getBytes(len(protocol.ParkedRouteLeaseID{}))
21721714 if err != nil {
2173- return NavigationDirective{}, ErrInvalidNavigation
1715+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
21741716 }
21751717 if err := r.done(); err != nil {
2176- return NavigationDirective{}, ErrInvalidNavigation
1718+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
21771719 }
2178- directive := NavigationDirective{Action: NavigationAction(value)}
1720+ directive := protocol.NavigationDirective{Action: protocol.NavigationAction(value)}
21791721 copy(directive.LeaseID[:], lease)
21801722 if MarshalNavigationDirective(directive) == nil {
2181- return NavigationDirective{}, ErrInvalidNavigation
1723+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
21821724 }
21831725 return directive, nil
21841726 }
21851727
21861728 // ValidateParkedRouteRequest enforces the closed action/target shape before a
21871729 // request reaches either side's route state machine.
2188-func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
2189- if request.RequestID == 0 || request.LeaseID.IsZero() {
2190- return ErrInvalidNavigation
2191- }
2192- switch request.Action {
2193- case ParkedRoutePrepare, ParkedRouteResume:
2194- if request.Target != nil {
2195- return ErrInvalidNavigation
2196- }
2197- case ParkedRouteSwitch:
2198- if request.Target == nil || validateWireRemoteTarget(*request.Target) != nil {
2199- return ErrInvalidNavigation
2200- }
2201- default:
2202- return ErrInvalidNavigation
2203- }
2204- return nil
2205-}
22061730
22071731 // MarshalParkedRouteRequest encodes one retained-route operation.
2208-func MarshalParkedRouteRequest(request ParkedRouteRequest) []byte {
2209- if ValidateParkedRouteRequest(request) != nil {
1732+func MarshalParkedRouteRequest(request protocol.ParkedRouteRequest) []byte {
1733+ if protocol.ValidateParkedRouteRequest(request) != nil {
22101734 return nil
22111735 }
22121736 w := payloadWriter{}
@@ -2218,35 +1742,35 @@ func MarshalParkedRouteRequest(request ParkedRouteRequest) []byte {
22181742 }
22191743
22201744 // UnmarshalParkedRouteRequest decodes one strict retained-route operation.
2221-func UnmarshalParkedRouteRequest(b []byte) (ParkedRouteRequest, error) {
1745+func UnmarshalParkedRouteRequest(b []byte) (protocol.ParkedRouteRequest, error) {
22221746 r := payloadReader{b: b}
22231747 requestID, err := r.getUint64()
22241748 if err != nil {
2225- return ParkedRouteRequest{}, ErrInvalidNavigation
1749+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
22261750 }
2227- lease, err := r.getBytes(len(ParkedRouteLeaseID{}))
1751+ lease, err := r.getBytes(len(protocol.ParkedRouteLeaseID{}))
22281752 if err != nil {
2229- return ParkedRouteRequest{}, ErrInvalidNavigation
1753+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
22301754 }
22311755 action, err := r.getUint8()
22321756 if err != nil {
2233- return ParkedRouteRequest{}, ErrInvalidNavigation
1757+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
22341758 }
22351759 target, policy, err := unmarshalRemoteTargetSection(&r)
22361760 if err != nil || policy != EnvironmentPolicyDaemonOwned || r.done() != nil {
2237- return ParkedRouteRequest{}, ErrInvalidNavigation
1761+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
22381762 }
2239- request := ParkedRouteRequest{RequestID: requestID, Action: ParkedRouteAction(action), Target: target}
1763+ request := protocol.ParkedRouteRequest{RequestID: requestID, Action: protocol.ParkedRouteAction(action), Target: target}
22401764 copy(request.LeaseID[:], lease)
2241- if ValidateParkedRouteRequest(request) != nil {
2242- return ParkedRouteRequest{}, ErrInvalidNavigation
1765+ if protocol.ValidateParkedRouteRequest(request) != nil {
1766+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
22431767 }
22441768 return request, nil
22451769 }
22461770
22471771 // MarshalParkedRouteResponse encodes one correlated retained-route outcome.
2248-func MarshalParkedRouteResponse(response ParkedRouteResponse) []byte {
2249- if response.RequestID == 0 || response.Status < ParkedRouteReady || response.Status > ParkedRouteStaleTarget {
1772+func MarshalParkedRouteResponse(response protocol.ParkedRouteResponse) []byte {
1773+ if response.RequestID == 0 || response.Status < protocol.ParkedRouteReady || response.Status > protocol.ParkedRouteStaleTarget {
22501774 return nil
22511775 }
22521776 w := payloadWriter{}
@@ -2256,19 +1780,19 @@ func MarshalParkedRouteResponse(response ParkedRouteResponse) []byte {
22561780 }
22571781
22581782 // UnmarshalParkedRouteResponse decodes one strict retained-route outcome.
2259-func UnmarshalParkedRouteResponse(b []byte) (ParkedRouteResponse, error) {
1783+func UnmarshalParkedRouteResponse(b []byte) (protocol.ParkedRouteResponse, error) {
22601784 r := payloadReader{b: b}
22611785 requestID, err := r.getUint64()
22621786 if err != nil {
2263- return ParkedRouteResponse{}, ErrInvalidNavigation
1787+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
22641788 }
22651789 status, err := r.getUint8()
22661790 if err != nil || r.done() != nil {
2267- return ParkedRouteResponse{}, ErrInvalidNavigation
1791+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
22681792 }
2269- response := ParkedRouteResponse{RequestID: requestID, Status: ParkedRouteStatus(status)}
1793+ response := protocol.ParkedRouteResponse{RequestID: requestID, Status: protocol.ParkedRouteStatus(status)}
22701794 if MarshalParkedRouteResponse(response) == nil {
2271- return ParkedRouteResponse{}, ErrInvalidNavigation
1795+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
22721796 }
22731797 return response, nil
22741798 }
M internal/ports/wire_final_test.go +11 -10
@@ -8,6 +8,7 @@ import (
88 "testing"
99
1010 "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {
@@ -81,7 +82,7 @@ func TestFinalHelloSemanticValidation(t *testing.T) {
8182 }
8283
8384 func TestFinalResizeGoldenStrict(t *testing.T) {
84- msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}}
85+ msg := protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}
8586 want := []byte{0, 80, 0, 24, 0, 0, 0, 0}
8687 got, err := MarshalResize(msg)
8788 if err != nil {
@@ -97,14 +98,14 @@ func TestFinalResizeGoldenStrict(t *testing.T) {
9798 assertAllPrefixesFail(t, got, UnmarshalResize)
9899 assertTrailingGarbageFails(t, got, UnmarshalResize)
99100 for _, size := range []domain.Size{{}, {Cols: 513, Rows: 512}} {
100- if got, err := MarshalResize(Resize{Size: size}); err == nil || got != nil {
101+ if got, err := MarshalResize(protocol.Resize{Size: size}); err == nil || got != nil {
101102 t.Fatalf("MarshalResize(%+v) = (%x, %v), want nil payload and error", size, got, err)
102103 }
103104 }
104105 }
105106
106107 func TestFinalOutputGoldenStrict(t *testing.T) {
107- msg := Output{
108+ msg := protocol.Output{
108109 Epoch: 1, Base: 0, New: 7, Echo: 8, ViewRevision: 9,
109110 Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("ok"),
110111 }
@@ -134,8 +135,8 @@ func TestFinalOutputGoldenStrict(t *testing.T) {
134135 }
135136
136137 func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {
137- valid := Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true}
138- for _, bad := range []Output{
138+ valid := protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true}
139+ for _, bad := range []protocol.Output{
139140 {},
140141 {Epoch: 1, Base: 0, New: 1, Size: valid.Size},
141142 {Epoch: 1, Base: 0, New: 1, Full: true},
@@ -170,7 +171,7 @@ func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {
170171 }
171172
172173 func TestFinalAckGoldenStrict(t *testing.T) {
173- msg := Ack{Epoch: 2, State: 7}
174+ msg := protocol.Ack{Epoch: 2, State: 7}
174175 want := []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 7}
175176 got, err := MarshalAck(msg)
176177 if err != nil {
@@ -180,7 +181,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
180181 t.Fatalf("Ack bytes = %x, want %x", got, want)
181182 }
182183 back, err := UnmarshalAck(got)
183- if err != nil || back != (Ack{Epoch: 2, State: 7}) {
184+ if err != nil || back != (protocol.Ack{Epoch: 2, State: 7}) {
184185 t.Fatalf("Ack = %+v, error %v", back, err)
185186 }
186187 assertAllPrefixesFail(t, got, UnmarshalAck)
@@ -188,7 +189,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
188189 if _, err := UnmarshalAck(make([]byte, 16)); err == nil {
189190 t.Fatal("UnmarshalAck accepted zero epoch")
190191 }
191- if got, err := MarshalAck(Ack{State: 7}); err == nil || got != nil {
192+ if got, err := MarshalAck(protocol.Ack{State: 7}); err == nil || got != nil {
192193 t.Fatalf("MarshalAck accepted zero epoch: payload=%x err=%v", got, err)
193194 }
194195 }
@@ -231,7 +232,7 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
231232 func TestAttachTargetExactTargetWireStrict(t *testing.T) {
232233 var lifecycle domain.SessionLifecycleID
233234 lifecycle[0] = 1
234- target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
235+ target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
235236 payload := MarshalAttachTarget(target)
236237 want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
237238 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)
@@ -245,7 +246,7 @@ func TestAttachTargetExactTargetWireStrict(t *testing.T) {
245246 assertAllPrefixesFail(t, payload, UnmarshalAttachTarget)
246247 assertTrailingGarbageFails(t, payload, UnmarshalAttachTarget)
247248 mismatched := target
248- mismatched.ExactTarget = &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "other"}
249+ mismatched.ExactTarget = &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "other"}
249250 if got := MarshalAttachTarget(mismatched); got != nil {
250251 t.Fatalf("MarshalAttachTarget accepted mismatched target: %x", got)
251252 }
M internal/ports/wire_test.go +17 -16
@@ -9,6 +9,7 @@ import (
99
1010 renderer "github.com/bnema/vev-vt"
1111 "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/protocol"
1213 "github.com/stretchr/testify/require"
1314 )
1415
@@ -243,15 +244,15 @@ func TestInputGoldenAndRoundTrip(t *testing.T) {
243244 func TestImagePushGoldenAndRoundTrip(t *testing.T) {
244245 tests := []struct {
245246 name string
246- msg ImagePush
247+ msg protocol.ImagePush
247248 want []byte
248249 }{
249250 {
250251 name: "png data",
251- msg: ImagePush{InputSeq: 7, Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},
252+ msg: protocol.ImagePush{InputSeq: 7, Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},
252253 want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x01, 0x02, 0x03},
253254 },
254- {name: "empty", msg: ImagePush{}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
255+ {name: "empty", msg: protocol.ImagePush{}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
255256 }
256257
257258 for _, tt := range tests {
@@ -288,17 +289,17 @@ func TestThemeGoldenAndRoundTrip(t *testing.T) {
288289 zeroes := make([]byte, 57)
289290 tests := []struct {
290291 name string
291- msg Theme
292+ msg protocol.Theme
292293 want []byte
293294 }{
294295 {
295296 name: "empty",
296- msg: Theme{},
297+ msg: protocol.Theme{},
297298 want: zeroes,
298299 },
299300 {
300301 name: "foreground only",
301- msg: Theme{
302+ msg: protocol.Theme{
302303 HasForeground: true,
303304 Foreground: renderer.RGB{R: 10, G: 20, B: 30},
304305 Background: renderer.RGB{R: 40, G: 50, B: 60},
@@ -307,14 +308,14 @@ func TestThemeGoldenAndRoundTrip(t *testing.T) {
307308 },
308309 {
309310 name: "light flag without known scheme",
310- msg: Theme{
311+ msg: protocol.Theme{
311312 Light: true,
312313 },
313314 want: append([]byte{0x10}, zeroes[1:]...),
314315 },
315316 {
316317 name: "full palette",
317- msg: Theme{
318+ msg: protocol.Theme{
318319 HasForeground: true,
319320 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
320321 HasBackground: true,
@@ -356,7 +357,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
356357 // A generation clear retains defaults/capabilities but has no palette bits.
357358 // This literal payload locks the existing 57-byte Theme layout while the
358359 // full definitive-palette golden above locks the final publication.
359- cleared := Theme{
360+ cleared := protocol.Theme{
360361 HasForeground: true,
361362 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
362363 HasBackground: true,
@@ -370,7 +371,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
370371 }
371372
372373 func TestResizeGoldenAndRoundTrip(t *testing.T) {
373- msg := Resize{Size: domain.Size{Cols: 100, Rows: 40}}
374+ msg := protocol.Resize{Size: domain.Size{Cols: 100, Rows: 40}}
374375 want := []byte{0x00, 0x64, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00}
375376
376377 got, err := MarshalResize(msg)
@@ -440,8 +441,8 @@ func TestPongGoldenAndRoundTrip(t *testing.T) {
440441 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
441442 got := MarshalWelcome(Welcome{
442443 SessionName: "work",
443- CommittedIdentity: &CommittedRouteIdentity{
444- Target: ExactSessionTarget{SessionName: "different"},
444+ CommittedIdentity: &protocol.CommittedRouteIdentity{
445+ Target: protocol.ExactSessionTarget{SessionName: "different"},
445446 },
446447 })
447448 require.Nil(t, got)
@@ -452,8 +453,8 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
452453 SessionID: "session",
453454 SessionName: "work",
454455 Ephemeral: true,
455- CommittedIdentity: &CommittedRouteIdentity{
456- Target: ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"},
456+ CommittedIdentity: &protocol.CommittedRouteIdentity{
457+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"},
457458 Ephemeral: true,
458459 },
459460 }
@@ -675,7 +676,7 @@ func TestErrorMsgGoldenAndRoundTrip(t *testing.T) {
675676 }
676677
677678 func TestOutputGoldenAndRoundTrip(t *testing.T) {
678- msg := Output{Epoch: 1, Base: 0, New: 2, Echo: 3, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("hello\n")}
679+ msg := protocol.Output{Epoch: 1, Base: 0, New: 2, Echo: 3, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("hello\n")}
679680 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a}
680681
681682 got, err := MarshalOutput(msg)
@@ -698,7 +699,7 @@ func TestOutputGoldenAndRoundTrip(t *testing.T) {
698699 }
699700
700701 func TestAckGoldenAndRoundTrip(t *testing.T) {
701- msg := Ack{Epoch: 1, State: 0x0102030405060708}
702+ msg := protocol.Ack{Epoch: 1, State: 0x0102030405060708}
702703 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
703704
704705 got, err := MarshalAck(msg)
A internal/protocol/navigation.go +106 -0
@@ -0,0 +1,106 @@
1+package protocol
2+
3+import "github.com/bnema/vev/internal/domain"
4+
5+type NavigationCapabilities uint8
6+
7+const (
8+ NavigationCapabilityHomePicker NavigationCapabilities = 1 << iota
9+ NavigationCapabilityBack
10+)
11+
12+type StartupOverlay uint8
13+
14+const (
15+ StartupOverlayNone StartupOverlay = iota
16+ StartupOverlaySessionPicker
17+)
18+
19+type NavigationAction uint8
20+
21+const (
22+ NavigationOpenHomePicker NavigationAction = 1
23+ NavigationBack NavigationAction = 2
24+)
25+
26+type ParkedRouteLeaseID [16]byte
27+
28+func (id ParkedRouteLeaseID) IsZero() bool { return id == ParkedRouteLeaseID{} }
29+
30+type NavigationDirective struct {
31+ Action NavigationAction
32+ LeaseID ParkedRouteLeaseID
33+}
34+
35+type ParkedRouteAction uint8
36+
37+const (
38+ ParkedRoutePrepare ParkedRouteAction = iota + 1
39+ ParkedRouteResume
40+ ParkedRouteSwitch
41+)
42+
43+type ParkedRouteRequest struct {
44+ RequestID uint64
45+ LeaseID ParkedRouteLeaseID
46+ Action ParkedRouteAction
47+ Target *domain.RemoteSessionTarget
48+}
49+
50+type ParkedRouteStatus uint8
51+
52+const (
53+ ParkedRouteReady ParkedRouteStatus = iota + 1
54+ ParkedRouteResumed
55+ ParkedRouteSwitched
56+ ParkedRouteRejected
57+ ParkedRouteExpired
58+ ParkedRouteStaleTarget
59+)
60+
61+type ParkedRouteResponse struct {
62+ RequestID uint64
63+ Status ParkedRouteStatus
64+}
65+
66+func validNavigationCapabilities(capabilities NavigationCapabilities) bool {
67+ return capabilities&^(NavigationCapabilityHomePicker|NavigationCapabilityBack) == 0
68+}
69+
70+func validStartupOverlay(overlay StartupOverlay) bool {
71+ return overlay == StartupOverlayNone || overlay == StartupOverlaySessionPicker
72+}
73+
74+func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOverlay, homePickerRoute bool) error {
75+ if !validNavigationCapabilities(capabilities) || !validStartupOverlay(overlay) {
76+ return ErrInvalidNavigation
77+ }
78+ home := capabilities&NavigationCapabilityHomePicker != 0
79+ back := capabilities&NavigationCapabilityBack != 0
80+ if (home && !homePickerRoute) || back != (overlay == StartupOverlaySessionPicker) {
81+ return ErrInvalidNavigation
82+ }
83+ if homePickerRoute && back {
84+ return ErrInvalidNavigation
85+ }
86+ return nil
87+}
88+
89+func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
90+ if request.RequestID == 0 || request.LeaseID.IsZero() {
91+ return ErrInvalidNavigation
92+ }
93+ switch request.Action {
94+ case ParkedRoutePrepare, ParkedRouteResume:
95+ if request.Target != nil {
96+ return ErrInvalidNavigation
97+ }
98+ case ParkedRouteSwitch:
99+ if request.Target == nil || validateRemoteTarget(*request.Target) != nil {
100+ return ErrInvalidNavigation
101+ }
102+ default:
103+ return ErrInvalidNavigation
104+ }
105+ return nil
106+}
R internal/ports/remote_preview.go → internal/protocol/preview.go +35 -1 renamed, 82% similar
@@ -1,4 +1,4 @@
1-package ports
1+package protocol
22
33 import (
44 "errors"
@@ -10,6 +10,40 @@ import (
1010 "github.com/bnema/vev/internal/domain"
1111 )
1212
13+// RemotePreviewSchemaVersion is independent from the attachment protocol version.
14+const RemotePreviewSchemaVersion uint16 = 1
15+
16+type RemotePreviewStatus uint8
17+
18+const (
19+ RemotePreviewOK RemotePreviewStatus = iota
20+ RemotePreviewUnavailable
21+ RemotePreviewNoSuchTarget
22+ RemotePreviewStale
23+ RemotePreviewMalformed
24+ RemotePreviewTooLarge
25+)
26+
27+type RemotePreviewRequest struct {
28+ Version uint16
29+ Target domain.RemoteSessionTarget
30+ Width uint16
31+ Height uint16
32+}
33+
34+// RemotePreview is a bounded row-major styled-cell viewport. It is never
35+// persisted, logged, or traced.
36+type RemotePreview struct {
37+ Version uint16
38+ Status RemotePreviewStatus
39+ LifecycleID domain.SessionLifecycleID
40+ TabID domain.TabStableID
41+ Revision uint64
42+ Width uint16
43+ Height uint16
44+ Cells []renderer.Cell
45+}
46+
1347 const (
1448 RemotePreviewMaxWidth = 256
1549 RemotePreviewMaxHeight = 128
R internal/ports/routes.go → internal/protocol/routes.go +21 -3 renamed, 93% similar
@@ -1,4 +1,4 @@
1-package ports
1+package protocol
22
33 import (
44 "errors"
@@ -9,6 +9,10 @@ import (
99 "github.com/bnema/vev/internal/domain"
1010 )
1111
12+// ErrInvalidRouteWire preserves the public error classification used by
13+// strict route codecs while route values live in the semantic protocol.
14+var ErrInvalidRouteWire = errors.New("invalid route wire message")
15+
1216 // RouteOrigin identifies how a client reached a daemon. The origin is client
1317 // composition metadata, not an authority granted to the daemon.
1418 type RouteOrigin uint8
@@ -181,10 +185,10 @@ type RouteAttentionSubscription struct {
181185 Targets []RouteAttentionTarget
182186 }
183187
184-func (r RouteRef) empty() bool { return r.Key == 0 && r.Generation == 0 }
188+func (r RouteRef) IsZero() bool { return r.Key == 0 && r.Generation == 0 }
185189
186190 func (r RouteRef) Validate() error {
187- if r.empty() {
191+ if r.IsZero() {
188192 return nil
189193 }
190194 if r.Key == 0 || r.Generation == 0 {
@@ -266,6 +270,13 @@ func (c SamePeerSwitchFailureCode) valid() bool {
266270 return c == SamePeerSwitchStaleTarget || c == SamePeerSwitchUnavailable
267271 }
268272
273+func (c SamePeerSwitchFailureCode) Validate() error {
274+ if !c.valid() {
275+ return ErrInvalidRouteWire
276+ }
277+ return nil
278+}
279+
269280 // SamePeerSwitchFailure leaves the source attachment unchanged. RequestID
270281 // rejects a delayed failure after a later user selection.
271282 type SamePeerSwitchFailure struct {
@@ -307,6 +318,13 @@ func (c RouteFailureCode) valid() bool {
307318 }
308319 }
309320
321+func (c RouteFailureCode) Validate() error {
322+ if !c.valid() {
323+ return ErrInvalidRouteWire
324+ }
325+ return nil
326+}
327+
310328 const (
311329 // RouteSnapshotMaxEntries bounds one immutable publication and the amount
312330 // of work a receiver performs before returning to its transport loop. The
A internal/protocol/session.go +101 -0
@@ -0,0 +1,101 @@
1+// Package protocol defines vev's typed, transport-neutral client/daemon
2+// contract. Binary framing and codecs live outside this package.
3+package protocol
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "math"
9+
10+ renderer "github.com/bnema/vev-vt"
11+ "github.com/bnema/vev/internal/domain"
12+)
13+
14+// MaxOutputDataLen is the largest decoded terminal byte stream accepted by one
15+// output publication in protocol version 37.
16+const MaxOutputDataLen = (16 << 20) - 55
17+
18+var (
19+ ErrInvalidSize = errors.New("ports: invalid terminal size")
20+ ErrInvalidOutput = errors.New("ports: invalid output")
21+ ErrInvalidAck = errors.New("ports: invalid ack")
22+ ErrInvalidNavigation = errors.New("ports: invalid navigation")
23+)
24+
25+type Resize struct {
26+ Size domain.Size
27+ PixelWidth int
28+ PixelHeight int
29+}
30+
31+func (m Resize) Geometry() domain.Geometry {
32+ return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels()
33+}
34+
35+type Theme struct {
36+ HasForeground bool
37+ Foreground renderer.RGB
38+ HasBackground bool
39+ Background renderer.RGB
40+ TrueColor bool
41+ SchemeKnown bool
42+ Light bool
43+ PaletteKnown uint16
44+ Palette [16]renderer.RGB
45+}
46+
47+type ImagePush struct {
48+ InputSeq uint64
49+ Mime string
50+ Data []byte
51+}
52+
53+type Ack struct {
54+ Epoch uint64
55+ State uint64
56+}
57+
58+type Output struct {
59+ Epoch uint64
60+ Base uint64
61+ New uint64
62+ Echo uint64
63+ ViewRevision uint64
64+ Size domain.Size
65+ Full bool
66+ Data []byte
67+}
68+
69+func ValidateOutput(m Output) error {
70+ if m.Epoch == 0 {
71+ return ErrInvalidOutput
72+ }
73+ if m.New == 0 {
74+ if m.Base != 0 || m.Full {
75+ return ErrInvalidOutput
76+ }
77+ } else if (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
78+ return ErrInvalidOutput
79+ }
80+ if err := ValidateSize(m.Size); err != nil {
81+ return fmt.Errorf("%w: size", ErrInvalidOutput)
82+ }
83+ if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxOutputDataLen {
84+ return ErrInvalidOutput
85+ }
86+ return nil
87+}
88+
89+func ValidateAck(m Ack) error {
90+ if m.Epoch == 0 {
91+ return ErrInvalidAck
92+ }
93+ return nil
94+}
95+
96+func validateRemoteTarget(target domain.RemoteSessionTarget) error {
97+ if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
98+ return errors.New("remote target string too long")
99+ }
100+ return target.Validate()
101+}
A internal/protocol/session_test.go +49 -0
@@ -0,0 +1,49 @@
1+package protocol
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt"
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestOutputSemanticValidation(t *testing.T) {
13+ valid := Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
14+ require.NoError(t, ValidateOutput(valid))
15+
16+ invalid := valid
17+ invalid.Epoch = 0
18+ require.ErrorIs(t, ValidateOutput(invalid), ErrInvalidOutput)
19+
20+ invalid = valid
21+ invalid.Data = make([]byte, MaxOutputDataLen+1)
22+ require.ErrorIs(t, ValidateOutput(invalid), ErrInvalidOutput)
23+}
24+
25+func TestRouteAndPreviewSemanticValidation(t *testing.T) {
26+ require.Error(t, ValidateRouteLabel("\x1b[31m", false))
27+ require.NoError(t, RouteRef{}.Validate())
28+ require.Error(t, (RouteRef{Key: 1}).Validate())
29+
30+ target := domain.RemoteSessionTarget{
31+ Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
32+ SessionName: "work", LiveTabID: "tab-1",
33+ }
34+ request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}
35+ require.NoError(t, ValidateRemotePreviewRequest(request))
36+ preview := RemotePreview{
37+ Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
38+ LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1,
39+ Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}},
40+ }
41+ require.NoError(t, ValidateRemotePreview(preview))
42+ preview.Cells[0].Rune = '\n'
43+ require.ErrorIs(t, ValidateRemotePreview(preview), ErrInvalidRemotePreview)
44+}
45+
46+func TestPresentationErrorsPreserveClassification(t *testing.T) {
47+ require.True(t, errors.Is(ValidateAck(Ack{}), ErrInvalidAck))
48+ require.True(t, errors.Is(ValidateParkedRouteRequest(ParkedRouteRequest{}), ErrInvalidNavigation))
49+}
R internal/ports/wire_size.go → internal/protocol/size.go +1 -1 renamed, 98% similar
@@ -1,4 +1,4 @@
1-package ports
1+package protocol
22
33 import (
44 "math"
M internal/testutil/replaytest/replay.go +4 -3
@@ -9,9 +9,10 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
14-func mustMarshalOutput(m ports.Output) []byte {
15+func mustMarshalOutput(m protocol.Output) []byte {
1516 payload, err := ports.MarshalOutput(m)
1617 if err != nil {
1718 panic(err)
@@ -20,8 +21,8 @@ func mustMarshalOutput(m ports.Output) []byte {
2021 }
2122
2223 var transcript = []ports.Frame{
23- {Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},
24- {Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},
24+ {Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},
25+ {Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},
2526 }
2627
2728 // Transcript returns a deep copy of the canonical replay transcript so an
M internal/usecase/client/client.go +67 -66
@@ -23,6 +23,7 @@ import (
2323 "github.com/bnema/vev-vt/protocol/terminalquery"
2424 "github.com/bnema/vev/internal/domain"
2525 "github.com/bnema/vev/internal/ports"
26+ "github.com/bnema/vev/internal/protocol"
2627 "github.com/bnema/vev/internal/usecase/theme"
2728 )
2829
@@ -55,7 +56,7 @@ func (e *DetachedError) Error() string { return "vev: " + e.Text }
5556 // without relying on another OSC response.
5657 type terminalThemeState struct {
5758 mu sync.Mutex
58- theme ports.Theme
59+ theme protocol.Theme
5960 }
6061
6162 func (s *terminalThemeState) setTrueColor(enabled bool) {
@@ -64,14 +65,14 @@ func (s *terminalThemeState) setTrueColor(enabled bool) {
6465 s.mu.Unlock()
6566 }
6667
67-func (s *terminalThemeState) update(update func(*ports.Theme)) ports.Theme {
68+func (s *terminalThemeState) update(update func(*protocol.Theme)) protocol.Theme {
6869 s.mu.Lock()
6970 defer s.mu.Unlock()
7071 update(&s.theme)
7172 return s.theme
7273 }
7374
74-func (s *terminalThemeState) snapshot() ports.Theme {
75+func (s *terminalThemeState) snapshot() protocol.Theme {
7576 s.mu.Lock()
7677 defer s.mu.Unlock()
7778 return s.theme
@@ -171,7 +172,7 @@ type Dependencies struct {
171172 Remote bool
172173 // Origin is explicit composition metadata for the initial route. A zero
173174 // value is normalized to local for direct package callers and old tests.
174- Origin ports.RouteOrigin
175+ Origin protocol.RouteOrigin
175176 OriginKey string
176177 }
177178
@@ -182,15 +183,15 @@ type AttachRequest struct {
182183 Intent uint8
183184 SessionName string
184185 Remote bool
185- Origin ports.RouteOrigin
186+ Origin protocol.RouteOrigin
186187 OriginKey string
187188 RemoteTarget *domain.RemoteSessionTarget
188- ExactTarget *ports.ExactSessionTarget
189+ ExactTarget *protocol.ExactSessionTarget
189190 PreferredTabID domain.TabStableID
190191 HostLabel string
191192 EnvironmentPolicy ports.EnvironmentPolicy
192- NavigationCapabilities ports.NavigationCapabilities
193- StartupOverlay ports.StartupOverlay
193+ NavigationCapabilities protocol.NavigationCapabilities
194+ StartupOverlay protocol.StartupOverlay
194195 }
195196
196197 // attachRoute captures the dialer, request, and resume token needed to
@@ -211,9 +212,9 @@ type Runner struct {
211212 remoteHostLearner ports.RemoteHostLearner
212213 attachHandoff AttachHandoffFunc
213214 remote bool
214- origin ports.RouteOrigin
215+ origin protocol.RouteOrigin
215216 ledger *routeLedger
216- routeFailure *ports.RouteNavigationFailure
217+ routeFailure *protocol.RouteNavigationFailure
217218
218219 inputMu sync.Mutex
219220 input *terminalInputPump
@@ -360,7 +361,7 @@ func validateAttachRequest(request AttachRequest) error {
360361 }
361362 }
362363 if request.OriginKey != "" {
363- if err := ports.ValidateRouteLabel(request.OriginKey, false); err != nil {
364+ if err := protocol.ValidateRouteLabel(request.OriginKey, false); err != nil {
364365 return fmt.Errorf("vev: invalid route origin key: %w", err)
365366 }
366367 }
@@ -378,7 +379,7 @@ func validateAttachRequest(request AttachRequest) error {
378379 if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil {
379380 return fmt.Errorf("vev: invalid session target: %w", err)
380381 }
381- if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {
382+ if err := protocol.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {
382383 return fmt.Errorf("vev: invalid navigation route: %w", err)
383384 }
384385 if request.RemoteTarget == nil {
@@ -452,7 +453,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
452453 routeNavigationResumeFallback := false
453454 var routeNavigationSelection *routeNavigationSelection
454455 var routeNavigationFallback *attachRoute
455- var routeNavigationAction *ports.RouteNavigationAction
456+ var routeNavigationAction *protocol.RouteNavigationAction
456457 backoff := defaultReconnectBackoff.initial
457458 themeState := &terminalThemeState{}
458459 var rememberOnce sync.Once
@@ -513,8 +514,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
513514 } else {
514515 attemptRequest.Intent = ports.IntentAttach
515516 }
516- attemptRequest.StartupOverlay = ports.StartupOverlayNone
517- attemptRequest.NavigationCapabilities &^= ports.NavigationCapabilityBack | ports.NavigationCapabilityHomePicker
517+ attemptRequest.StartupOverlay = protocol.StartupOverlayNone
518+ attemptRequest.NavigationCapabilities &^= protocol.NavigationCapabilityBack | protocol.NavigationCapabilityHomePicker
518519 resumeToken = route.resumeToken
519520 returnResumeFallback = route.resumeToken != 0
520521 remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote)
@@ -524,8 +525,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
524525 dialer = homeRoute.dialer
525526 attemptRequest = homeRoute.request
526527 attemptRequest.Intent = ports.IntentAttach
527- attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack
528- attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker
528+ attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityBack
529+ attemptRequest.StartupOverlay = protocol.StartupOverlaySessionPicker
529530 attemptRequest.RemoteTarget = nil
530531 attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
531532 attemptRequest.Remote = homeRoute.request.Remote || r.remote
@@ -556,8 +557,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
556557 }
557558 request := cloneAttachRequest(homeRoute.request)
558559 request.Intent = ports.IntentAttach
559- request.NavigationCapabilities = ports.NavigationCapabilityBack
560- request.StartupOverlay = ports.StartupOverlaySessionPicker
560+ request.NavigationCapabilities = protocol.NavigationCapabilityBack
561+ request.StartupOverlay = protocol.StartupOverlaySessionPicker
561562 request.RemoteTarget = nil
562563 request.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
563564 request.Remote = false
@@ -739,10 +740,10 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
739740 // Back is transient to the picker overlay. Re-derive the home-picker
740741 // capability from the selected route instead of trusting request
741742 // metadata, which may have been cleared after a daemon-side switch.
742- attemptRequest.StartupOverlay = ports.StartupOverlayNone
743+ attemptRequest.StartupOverlay = protocol.StartupOverlayNone
743744 attemptRequest.NavigationCapabilities = 0
744- if homeRoute != nil && selection.selected.presentation.kind == ports.RouteKindRemote {
745- attemptRequest.NavigationCapabilities = ports.NavigationCapabilityHomePicker
745+ if homeRoute != nil && selection.selected.presentation.kind == protocol.RouteKindRemote {
746+ attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
746747 }
747748 resumeToken = selection.selected.resumeToken
748749 routeNavigationPending = true
@@ -754,7 +755,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
754755 }
755756 if result.action != 0 {
756757 switch result.action {
757- case ports.NavigationOpenHomePicker:
758+ case protocol.NavigationOpenHomePicker:
758759 // The daemon only sends this action after accepting a Hello that
759760 // advertised Home support. The durable client-side prerequisite is
760761 // the captured route itself; request metadata may have been rebased by
@@ -770,7 +771,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
770771 homeNavigationPending = true
771772 enterHomePicker()
772773 continue
773- case ports.NavigationBack:
774+ case protocol.NavigationBack:
774775 // As with Home, the daemon accepted Back in Hello before sending
775776 // this action. The retained concrete route is the durable client
776777 // prerequisite; request metadata may have been rebased meanwhile.
@@ -796,7 +797,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
796797 }
797798 routeRequest.Intent = ports.IntentAttach
798799 routeRequest.NavigationCapabilities = 0
799- routeRequest.StartupOverlay = ports.StartupOverlayNone
800+ routeRequest.StartupOverlay = protocol.StartupOverlayNone
800801 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}
801802 }
802803 }
@@ -832,9 +833,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
832833 nextRequest.ExactTarget = result.target.ExactTarget
833834 }
834835 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned) {
835- nextRequest.NavigationCapabilities |= ports.NavigationCapabilityHomePicker
836+ nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
836837 }
837- if attemptRequest.StartupOverlay == ports.StartupOverlaySessionPicker {
838+ if attemptRequest.StartupOverlay == protocol.StartupOverlaySessionPicker {
838839 returnRoute = nil
839840 returnResumeFallback = false
840841 }
@@ -882,7 +883,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
882883 if routeNavigationPending && routeNavigationFallback != nil {
883884 if routeNavigationAction != nil {
884885 action := *routeNavigationAction
885- r.routeFailure = &ports.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: routeFailureCode(result.err)}
886+ r.routeFailure = &protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: routeFailureCode(result.err)}
886887 routeNavigationAction = nil
887888 }
888889 route := *routeNavigationFallback
@@ -1084,14 +1085,14 @@ type attachAttempt struct {
10841085 type attachResult struct {
10851086 resumeToken uint64
10861087 sessionName string
1087- committedIdentity *ports.CommittedRouteIdentity
1088- routePosition *ports.RoutePosition
1088+ committedIdentity *protocol.CommittedRouteIdentity
1089+ routePosition *protocol.RoutePosition
10891090 welcomed bool
10901091 transportClosed bool
10911092 target *ports.AttachTarget
1092- action ports.NavigationAction
1093+ action protocol.NavigationAction
10931094 homePickerTarget bool
1094- routeAction *ports.RouteNavigationAction
1095+ routeAction *protocol.RouteNavigationAction
10951096 err error
10961097 }
10971098
@@ -1213,7 +1214,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12131214 themeState.setTrueColor(trueColor)
12141215 exactTarget := request.ExactTarget
12151216 if exactTarget == nil && request.RemoteTarget != nil {
1216- exactTarget = &ports.ExactSessionTarget{
1217+ exactTarget = &protocol.ExactSessionTarget{
12171218 LifecycleID: request.RemoteTarget.LifecycleID,
12181219 SessionName: request.RemoteTarget.SessionName,
12191220 }
@@ -1257,8 +1258,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12571258 }, transition); err != nil {
12581259 return handshakeFailure("awaiting welcome", err)
12591260 }
1260- var committedIdentity *ports.CommittedRouteIdentity
1261- var routePosition *ports.RoutePosition
1261+ var committedIdentity *protocol.CommittedRouteIdentity
1262+ var routePosition *protocol.RoutePosition
12621263 result := func(welcomed bool, err error) attachResult {
12631264 return attachResult{
12641265 resumeToken: resumeToken,
@@ -1391,7 +1392,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
13911392 delete(timers, id)
13921393 }
13931394 }
1394- publish := func(snapshot ports.Theme) error {
1395+ publish := func(snapshot protocol.Theme) error {
13951396 frame := ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(snapshot)}
13961397 // The initial cleared snapshot is sent before the sender exists. This
13971398 // keeps the generation publication ordered before the first query and
@@ -1417,7 +1418,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
14171418 return fmt.Errorf("publishing theme: %w", err)
14181419 }
14191420 case paletteActionPublishFinal:
1420- themeState.update(func(current *ports.Theme) { *current = action.theme })
1421+ themeState.update(func(current *protocol.Theme) { *current = action.theme })
14211422 if err := publish(action.theme); err != nil {
14221423 return fmt.Errorf("publishing theme: %w", err)
14231424 }
@@ -1487,8 +1488,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
14871488 var nextSamePeerRequestID uint64
14881489 type parkedRoutePending struct {
14891490 requestID uint64
1490- action ports.ParkedRouteAction
1491- leaseID ports.ParkedRouteLeaseID
1491+ action protocol.ParkedRouteAction
1492+ leaseID protocol.ParkedRouteLeaseID
14921493 fallbackTarget *ports.AttachTarget
14931494 timer ports.Timer
14941495 }
@@ -1502,7 +1503,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
15021503 // Before the asynchronous sender starts, preserve transport ordering by
15031504 // requesting the reset synchronously after the initial Theme publication.
15041505 resetErr := sendHandshake(func() error {
1505- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1506+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
15061507 if err != nil {
15071508 return err
15081509 }
@@ -1628,7 +1629,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16281629 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
16291630 }
16301631 geometry = geometry.NormalizePixels()
1631- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1632+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
16321633 if err != nil {
16331634 return fmt.Errorf("encoding terminal size for parked route: %w", err)
16341635 }
@@ -1639,12 +1640,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16391640 return context.Canceled
16401641 }
16411642 }
1642- sendParkedRouteRequest := func(action ports.ParkedRouteAction, leaseID ports.ParkedRouteLeaseID, target *domain.RemoteSessionTarget) error {
1643+ sendParkedRouteRequest := func(action protocol.ParkedRouteAction, leaseID protocol.ParkedRouteLeaseID, target *domain.RemoteSessionTarget) error {
16431644 if parkedPending != nil {
16441645 return errors.New("vev: parked-route request already pending")
16451646 }
16461647 nextParkedRequestID++
1647- request := ports.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1648+ request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
16481649 payload := ports.MarshalParkedRouteRequest(request)
16491650 if payload == nil {
16501651 return errors.New("vev: invalid parked-route request")
@@ -1660,7 +1661,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16601661 return context.Canceled
16611662 }
16621663 }
1663- handleParkedPicker := func(leaseID ports.ParkedRouteLeaseID) (attachResult, bool) {
1664+ handleParkedPicker := func(leaseID protocol.ParkedRouteLeaseID) (attachResult, bool) {
16641665 pickerCtx, cancelPicker := context.WithCancel(ctx)
16651666 pickerDone := make(chan struct{})
16661667 go func() {
@@ -1687,7 +1688,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16871688 if err := sendParkedRouteSize(); err != nil {
16881689 return welcomedResult(err), true
16891690 }
1690- if err := sendParkedRouteRequest(ports.ParkedRouteSwitch, leaseID, &target); err != nil {
1691+ if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil {
16911692 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true
16921693 }
16931694 fallback := *selection.target
@@ -1704,7 +1705,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17041705 if err := sendParkedRouteSize(); err != nil {
17051706 return welcomedResult(err), true
17061707 }
1707- if err := sendParkedRouteRequest(ports.ParkedRouteResume, leaseID, nil); err != nil {
1708+ if err := sendParkedRouteRequest(protocol.ParkedRouteResume, leaseID, nil); err != nil {
17081709 return welcomedResult(fmt.Errorf("vev: resuming parked route: %w", err)), true
17091710 }
17101711 return attachResult{}, false
@@ -1725,7 +1726,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17251726 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
17261727 }
17271728 geometry = geometry.NormalizePixels()
1728- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1729+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
17291730 if err != nil {
17301731 return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)
17311732 }
@@ -1761,8 +1762,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17611762 }
17621763 return nil
17631764 }
1764- sendRouteFailure := func(action ports.RouteNavigationAction, code ports.RouteFailureCode) error {
1765- payload, err := ports.MarshalRouteNavigationFailure(ports.RouteNavigationFailure{
1765+ sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {
1766+ payload, err := ports.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{
17661767 Key: action.Key,
17671768 Generation: action.Generation,
17681769 Code: code,
@@ -1861,7 +1862,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18611862 }
18621863 case event := <-paletteEvents:
18631864 if event.kind == paletteEventScheme {
1864- retained := themeState.update(func(current *ports.Theme) {
1865+ retained := themeState.update(func(current *protocol.Theme) {
18651866 current.SchemeKnown = true
18661867 current.Light = event.light
18671868 })
@@ -1965,7 +1966,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19651966 }
19661967 nextSamePeerRequestID++
19671968 request := a.runner.ledger.samePeerHandoff(request, target)
1968- payload, err := ports.MarshalSamePeerSwitchRequest(ports.SamePeerSwitchRequest{
1969+ payload, err := ports.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{
19691970 RequestID: nextSamePeerRequestID,
19701971 Target: *target.ExactTarget,
19711972 PreferredTabID: request.PreferredTabID,
@@ -1996,26 +1997,26 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19961997 pending := *parkedPending
19971998 clearParkedPending()
19981999 switch pending.action {
1999- case ports.ParkedRoutePrepare:
2000- if response.Status != ports.ParkedRouteReady {
2000+ case protocol.ParkedRoutePrepare:
2001+ if response.Status != protocol.ParkedRouteReady {
20012002 fallback := welcomedResult(nil)
2002- fallback.action = ports.NavigationOpenHomePicker
2003+ fallback.action = protocol.NavigationOpenHomePicker
20032004 return fallback
20042005 }
20052006 if selection, done := handleParkedPicker(pending.leaseID); done {
20062007 return selection
20072008 }
2008- case ports.ParkedRouteResume:
2009- if response.Status != ports.ParkedRouteResumed {
2009+ case protocol.ParkedRouteResume:
2010+ if response.Status != protocol.ParkedRouteResumed {
20102011 return welcomedResult(errors.New("vev: parked route could not resume"))
20112012 }
20122013 awaitParkedFull()
2013- case ports.ParkedRouteSwitch:
2014+ case protocol.ParkedRouteSwitch:
20142015 switch response.Status {
2015- case ports.ParkedRouteSwitched:
2016+ case protocol.ParkedRouteSwitched:
20162017 awaitParkedFull()
20172018 continue
2018- case ports.ParkedRouteStaleTarget:
2019+ case protocol.ParkedRouteStaleTarget:
20192020 if transition != nil {
20202021 transition.stop()
20212022 }
@@ -2038,12 +2039,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20382039 return welcomedResult(fmt.Errorf("vev: decoding navigation action: %w", derr))
20392040 }
20402041 _, datagramRoute := transport.(ports.DatagramTransport)
2041- if directive.Action == ports.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
2042+ if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
20422043 stopForeground()
20432044 if err := flushSender(); err != nil {
20442045 return welcomedResult(fmt.Errorf("vev: parking remote foreground: %w", err))
20452046 }
2046- if err := sendParkedRouteRequest(ports.ParkedRoutePrepare, directive.LeaseID, nil); err != nil {
2047+ if err := sendParkedRouteRequest(protocol.ParkedRoutePrepare, directive.LeaseID, nil); err != nil {
20472048 return welcomedResult(fmt.Errorf("vev: preparing parked route: %w", err))
20482049 }
20492050 continue
@@ -2057,14 +2058,14 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20572058 return welcomedResult(fmt.Errorf("vev: decoding route navigation action: %w", derr))
20582059 }
20592060 if a.runner.ledger == nil {
2060- if ferr := sendRouteFailure(action, ports.RouteFailureUnavailable); ferr != nil {
2061+ if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
20612062 return welcomedResult(ferr)
20622063 }
20632064 continue
20642065 }
20652066 _, valid, noOp := a.runner.ledger.navigationRecord(action)
20662067 if !valid {
2067- if ferr := sendRouteFailure(action, ports.RouteFailureStaleSelection); ferr != nil {
2068+ if ferr := sendRouteFailure(action, protocol.RouteFailureStaleSelection); ferr != nil {
20682069 return welcomedResult(ferr)
20692070 }
20702071 continue
@@ -2244,7 +2245,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
22442245 if state == 0 {
22452246 return true
22462247 }
2247- payload, err := ports.MarshalAck(ports.Ack{Epoch: epoch, State: state})
2248+ payload, err := ports.MarshalAck(protocol.Ack{Epoch: epoch, State: state})
22482249 if err != nil {
22492250 select {
22502251 case errCh <- fmt.Errorf("encoding output ACK: %w", err):
@@ -2802,7 +2803,7 @@ func (p *stdinPump) run() {
28022803 },
28032804 sendImage: func(mime string, data []byte) {
28042805 inputSeq++
2805- send(ports.Frame{Type: ports.MsgImagePush, Payload: ports.MarshalImagePush(ports.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})
2806+ send(ports.Frame{Type: ports.MsgImagePush, Payload: ports.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})
28062807 },
28072808 next: coalescer.Scan,
28082809 }
@@ -2974,7 +2975,7 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- po
29742975 return
29752976 }
29762977 geometry = geometry.NormalizePixels()
2977- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
2978+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
29782979 if err != nil {
29792980 log.Error("encoding terminal resize", "error", err)
29802981 continue
M internal/usecase/client/client_test.go +100 -99
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
2020 portsmocks "github.com/bnema/vev/internal/ports/mocks"
21+ "github.com/bnema/vev/internal/protocol"
2122 "github.com/bnema/vev/internal/usecase/client"
2223 )
2324
@@ -436,8 +437,8 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
436437 {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
437438 SessionID: "s1",
438439 SessionName: "main",
439- CommittedIdentity: &ports.CommittedRouteIdentity{
440- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},
440+ CommittedIdentity: &protocol.CommittedRouteIdentity{
441+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},
441442 },
442443 }))},
443444 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
@@ -446,13 +447,13 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
446447 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{
447448 Intent: ports.IntentAttach,
448449 SessionName: "main",
449- Origin: ports.RouteOriginLocal,
450+ Origin: protocol.RouteOriginLocal,
450451 OriginKey: "local",
451452 })
452453 require.NoError(t, err)
453454
454- var snapshot ports.RecentRouteSnapshot
455- var subscription ports.RouteAttentionSubscription
455+ var snapshot protocol.RecentRouteSnapshot
456+ var subscription protocol.RouteAttentionSubscription
456457 foundSnapshot := false
457458 foundSubscription := false
458459 for _, frame := range tr.Sends() {
@@ -486,26 +487,26 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
486487 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
487488 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
488489 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
489- CommittedIdentity: &ports.CommittedRouteIdentity{
490- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
490+ CommittedIdentity: &protocol.CommittedRouteIdentity{
491+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
491492 },
492493 }))
493494 }
494- secondTarget := ports.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}
495+ secondTarget := protocol.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}
495496 first := &recordingTransport{recvs: []recvItem{
496497 {f: welcome("first", firstLifecycle)},
497498 {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
498499 Session: "second", Intent: ports.IntentAttach, ExactTarget: &secondTarget,
499500 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, SamePeer: true,
500501 }))},
501- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{Target: secondTarget}))},
502+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))},
502503 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
503504 }}
504505 dialer := &sequenceDialer{trs: []ports.Transport{first}}
505506
506507 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
507508 Intent: ports.IntentAttach, SessionName: "first",
508- Origin: ports.RouteOriginLocal, OriginKey: "local",
509+ Origin: protocol.RouteOriginLocal, OriginKey: "local",
509510 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
510511 })
511512
@@ -524,8 +525,8 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) {
524525 request := client.AttachRequest{
525526 Intent: ports.IntentAttach, SessionName: "work", Remote: true,
526527 RemoteTarget: target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
527- NavigationCapabilities: ports.NavigationCapabilityHomePicker,
528- StartupOverlay: ports.StartupOverlayNone,
528+ NavigationCapabilities: protocol.NavigationCapabilityHomePicker,
529+ StartupOverlay: protocol.StartupOverlayNone,
529530 }
530531 tr := &recordingTransport{recvs: []recvItem{
531532 {f: welcomeFrame(0)},
@@ -548,12 +549,12 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
548549
549550 sourceLifecycle := domain.SessionLifecycleID{1}
550551 targetLifecycle := domain.SessionLifecycleID{2}
551- target := ports.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}
552+ target := protocol.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}
552553 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
553554 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
554555 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
555- CommittedIdentity: &ports.CommittedRouteIdentity{
556- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
556+ CommittedIdentity: &protocol.CommittedRouteIdentity{
557+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
557558 },
558559 }))
559560 }
@@ -572,7 +573,7 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
572573
573574 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
574575 Intent: ports.IntentAttach, SessionName: "source",
575- Origin: ports.RouteOriginLocal, OriginKey: "local",
576+ Origin: protocol.RouteOriginLocal, OriginKey: "local",
576577 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
577578 })
578579
@@ -589,7 +590,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
589590 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
590591 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
591592 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
592- CommittedIdentity: &ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
593+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
593594 }))
594595 }
595596 remoteTarget := domain.RemoteSessionTarget{
@@ -605,7 +606,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
605606 }}
606607 remote := &recordingTransport{recvs: []recvItem{
607608 {f: welcome("work", remoteLifecycle)},
608- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
609+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
609610 }}
610611 local2 := &recordingTransport{recvs: []recvItem{
611612 {f: welcome("local", localLifecycle)},
@@ -615,10 +616,10 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
615616 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
616617 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
617618 deps.AttachHandoff = func(ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
618- target := ports.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
619+ target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
619620 return remoteDialer, client.AttachRequest{
620621 Intent: ports.IntentAttach, SessionName: "work", Remote: true,
621- Origin: ports.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,
622+ Origin: protocol.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,
622623 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
623624 // Deliberately omit the copied Home capability. The accepted server
624625 // action remains valid because Runner retained the concrete home route.
@@ -626,15 +627,15 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
626627 }
627628
628629 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
629- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
630+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
630631 }))
631- require.Equal(t, ports.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)
632+ require.Equal(t, protocol.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)
632633 }
633634
634635 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
635636 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
636637 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
637- CommittedIdentity: &ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
638+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
638639 }))
639640 }
640641
@@ -661,15 +662,15 @@ func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clo
661662 selection := *target.RemoteTarget
662663 return dialer, client.AttachRequest{
663664 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
664- Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint,
665+ Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
665666 RemoteTarget: &selection, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
666667 }, nil
667668 }
668669 return deps
669670 }
670671
671-func hybridParkedRequestHandler(requests chan<- ports.ParkedRouteRequest, requestErrors chan<- error, signals map[ports.ParkedRouteAction]chan struct{}) func(ports.Frame) {
672- once := make(map[ports.ParkedRouteAction]*sync.Once, len(signals))
672+func hybridParkedRequestHandler(requests chan<- protocol.ParkedRouteRequest, requestErrors chan<- error, signals map[protocol.ParkedRouteAction]chan struct{}) func(ports.Frame) {
673+ once := make(map[protocol.ParkedRouteAction]*sync.Once, len(signals))
673674 for action := range signals {
674675 once[action] = &sync.Once{}
675676 }
@@ -711,20 +712,20 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
711712 localInitial := hybridLocalBootstrap(localLifecycle, sourceTarget)
712713 parkSent := make(chan struct{})
713714 switchSent := make(chan struct{})
714- parkedRequests := make(chan ports.ParkedRouteRequest, 2)
715+ parkedRequests := make(chan protocol.ParkedRouteRequest, 2)
715716 parkedRequestErrors := make(chan error, 2)
716717 remote := &recordingTransport{recvs: []recvItem{
717718 {f: hybridWelcomeFrame("source", remoteSourceLifecycle)},
718- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
719- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},
720- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},
721- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteSwitched}))},
722- {f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},
719+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
720+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
721+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},
722+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}))},
723+ {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},
723724 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
724725 }}
725- remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{
726- ports.ParkedRoutePrepare: parkSent,
727- ports.ParkedRouteSwitch: switchSent,
726+ remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
727+ protocol.ParkedRoutePrepare: parkSent,
728+ protocol.ParkedRouteSwitch: switchSent,
728729 })
729730 localPicker := &recordingTransport{recvs: []recvItem{
730731 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -744,7 +745,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
744745 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
745746
746747 err := runTestClient(context.Background(), deps, client.AttachRequest{
747- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
748+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
748749 })
749750
750751 require.NoError(t, err)
@@ -755,8 +756,8 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
755756 }
756757 prepareRequest := <-parkedRequests
757758 switchRequest := <-parkedRequests
758- require.Equal(t, ports.ParkedRoutePrepare, prepareRequest.Action)
759- require.Equal(t, ports.ParkedRouteSwitch, switchRequest.Action)
759+ require.Equal(t, protocol.ParkedRoutePrepare, prepareRequest.Action)
760+ require.Equal(t, protocol.ParkedRouteSwitch, switchRequest.Action)
760761 require.NotNil(t, switchRequest.Target)
761762 require.Equal(t, targetTarget, *switchRequest.Target)
762763 frames := remote.Sends()
@@ -767,7 +768,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
767768 }
768769 request, requestErr := ports.UnmarshalParkedRouteRequest(frame.Payload)
769770 require.NoError(t, requestErr)
770- if request.Action == ports.ParkedRouteSwitch {
771+ if request.Action == protocol.ParkedRouteSwitch {
771772 switchIndex = i
772773 break
773774 }
@@ -801,13 +802,13 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
801802 parkedRequestErrors := make(chan error, 1)
802803 sourceRemote := &recordingTransport{recvs: []recvItem{
803804 {f: hybridWelcomeFrame("source", sourceLifecycle)},
804- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
805- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: prepareSent},
806- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteExpired})), wait: switchSent},
805+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
806+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: prepareSent},
807+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteExpired})), wait: switchSent},
807808 }}
808- sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{
809- ports.ParkedRoutePrepare: prepareSent,
810- ports.ParkedRouteSwitch: switchSent,
809+ sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
810+ protocol.ParkedRoutePrepare: prepareSent,
811+ protocol.ParkedRouteSwitch: switchSent,
811812 })
812813 localPicker := &recordingTransport{recvs: []recvItem{
813814 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -827,7 +828,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
827828 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
828829
829830 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
830- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
831+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
831832 }))
832833 select {
833834 case requestErr := <-parkedRequestErrors:
@@ -878,12 +879,12 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
878879 remote := &recordingTransport{
879880 recvs: []recvItem{
880881 {f: hybridWelcomeFrame("work", remoteLifecycle)},
881- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
882+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
882883 },
883884 stall: remoteClosed,
884885 }
885- remote.onSend = hybridParkedRequestHandler(nil, nil, map[ports.ParkedRouteAction]chan struct{}{
886- ports.ParkedRoutePrepare: prepareSent,
886+ remote.onSend = hybridParkedRequestHandler(nil, nil, map[protocol.ParkedRouteAction]chan struct{}{
887+ protocol.ParkedRoutePrepare: prepareSent,
887888 })
888889
889890 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}
@@ -899,7 +900,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
899900 result := make(chan error, 1)
900901 go func() {
901902 result <- runTestClient(ctx, deps, client.AttachRequest{
902- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
903+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
903904 })
904905 }()
905906 select {
@@ -949,26 +950,26 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
949950 parkedRequestErrors := make(chan error, 2)
950951 remote := &recordingTransport{recvs: []recvItem{
951952 {f: hybridWelcomeFrame("work", remoteLifecycle)},
952- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
953- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},
954- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteResumed})), wait: resumeSent},
955- {f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},
953+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
954+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
955+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeSent},
956+ {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},
956957 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
957958 }}
958- remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{
959- ports.ParkedRoutePrepare: parkSent,
960- ports.ParkedRouteResume: resumeSent,
959+ remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
960+ protocol.ParkedRoutePrepare: parkSent,
961+ protocol.ParkedRouteResume: resumeSent,
961962 })
962963 localPicker := &recordingTransport{recvs: []recvItem{
963964 {f: hybridWelcomeFrame("local", localLifecycle)},
964- {f: navigationDirectiveFrame(ports.NavigationBack)},
965+ {f: navigationDirectiveFrame(protocol.NavigationBack)},
965966 }}
966967 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
967968 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
968969 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
969970
970971 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
971- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
972+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
972973 }))
973974 select {
974975 case requestErr := <-parkedRequestErrors:
@@ -1000,12 +1001,12 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
10001001 defer close(keepSourceOpen)
10011002 sourceRemote := &recordingTransport{recvs: []recvItem{
10021003 {f: hybridWelcomeFrame("source", sourceLifecycle)},
1003- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
1004- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},
1004+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
1005+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
10051006 {err: io.EOF, wait: keepSourceOpen},
10061007 }}
1007- sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{
1008- ports.ParkedRoutePrepare: parkSent,
1008+ sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
1009+ protocol.ParkedRoutePrepare: parkSent,
10091010 })
10101011 localPicker := &recordingTransport{recvs: []recvItem{
10111012 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -1027,7 +1028,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
10271028 })
10281029
10291030 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
1030- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",
1031+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
10311032 }))
10321033 select {
10331034 case requestErr := <-parkedRequestErrors:
@@ -1053,8 +1054,8 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
10531054 SessionName: name,
10541055 ResumeToken: token,
10551056 Capabilities: ports.CapabilityResume,
1056- CommittedIdentity: &ports.CommittedRouteIdentity{
1057- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1057+ CommittedIdentity: &protocol.CommittedRouteIdentity{
1058+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
10581059 },
10591060 }))
10601061 }
@@ -1076,28 +1077,28 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
10761077 }}
10771078 local2 := &recordingTransport{recvs: []recvItem{
10781079 {f: welcome("test", localLifecycle, 11)},
1079- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{
1080- Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
1080+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1081+ Target: protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
10811082 }))},
1082- {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))},
1083+ {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))},
10831084 }}
10841085 local3 := &recordingTransport{recvs: []recvItem{
10851086 {f: welcome("test", localLifecycle, 11)},
1086- {f: navigationDirectiveFrame(ports.NavigationBack)},
1087+ {f: navigationDirectiveFrame(protocol.NavigationBack)},
10871088 }}
10881089 remote1 := &recordingTransport{recvs: []recvItem{
10891090 {f: welcome("remote-manual", remoteLifecycle, 22)},
1090- {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))},
1091+ {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(protocol.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))},
10911092 }}
10921093 remote2 := &recordingTransport{recvs: []recvItem{
10931094 {f: welcome("remote-manual", remoteLifecycle, 22)},
1094- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{
1095- Target: ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"},
1095+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1096+ Target: protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"},
10961097 }))},
1097- {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(ports.RoutePosition{
1098- Target: ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, ActiveTabID: "tab-2",
1098+ {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(protocol.RoutePosition{
1099+ Target: protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, ActiveTabID: "tab-2",
10991100 }))},
1100- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},
1101+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
11011102 }}
11021103 remote3 := &recordingTransport{recvs: []recvItem{
11031104 {f: welcome("remote-new", remoteNewLifecycle, 22)},
@@ -1111,32 +1112,32 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
11111112 require.Equal(t, remoteHandoff, target)
11121113 return remoteDialer, client.AttachRequest{
11131114 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
1114- Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint,
1115+ Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
11151116 RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
11161117 }, nil
11171118 }
11181119
11191120 err := runTestClient(context.Background(), deps, client.AttachRequest{
1120- Intent: ports.IntentAttach, SessionName: "test", Origin: ports.RouteOriginLocal, OriginKey: "local",
1121+ Intent: ports.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",
11211122 })
11221123 remoteHello := helloFromSend(t, remote2)
11231124 require.NoError(t, err)
11241125
1125- require.Equal(t, ports.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities)
1126+ require.Equal(t, protocol.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities)
11261127 remoteNewHello := helloFromSend(t, remote3)
11271128 require.Equal(t, "remote-new", remoteNewHello.Name)
1128- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, remoteNewHello.ExactTarget)
1129+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, remoteNewHello.ExactTarget)
11291130 }
11301131
1131-func navigationDirectiveFrame(action ports.NavigationAction) ports.Frame {
1132- directive := ports.NavigationDirective{Action: action}
1133- if action == ports.NavigationOpenHomePicker {
1134- directive.LeaseID = ports.ParkedRouteLeaseID{1}
1132+func navigationDirectiveFrame(action protocol.NavigationAction) ports.Frame {
1133+ directive := protocol.NavigationDirective{Action: action}
1134+ if action == protocol.NavigationOpenHomePicker {
1135+ directive.LeaseID = protocol.ParkedRouteLeaseID{1}
11351136 }
11361137 return frameOf(ports.MsgNavigationAction, ports.MarshalNavigationDirective(directive))
11371138 }
11381139
1139-func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte {
1140+func mustMarshalCommittedIdentity(identity protocol.CommittedRouteIdentity) []byte {
11401141 payload, err := ports.MarshalCommittedRouteIdentity(identity)
11411142 if err != nil {
11421143 panic(err)
@@ -1144,7 +1145,7 @@ func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte
11441145 return payload
11451146 }
11461147
1147-func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte {
1148+func mustMarshalRouteAction(action protocol.RouteNavigationAction) []byte {
11481149 payload, err := ports.MarshalRouteNavigationAction(action)
11491150 if err != nil {
11501151 panic(err)
@@ -1152,7 +1153,7 @@ func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte {
11521153 return payload
11531154 }
11541155
1155-func mustMarshalRoutePosition(position ports.RoutePosition) []byte {
1156+func mustMarshalRoutePosition(position protocol.RoutePosition) []byte {
11561157 payload, err := ports.MarshalRoutePosition(position)
11571158 if err != nil {
11581159 panic(err)
@@ -1256,7 +1257,7 @@ func TestAttachHappyPath(t *testing.T) {
12561257 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
12571258 unblock := scriptRecv(tr,
12581259 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},
1259- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},
1260+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},
12601261 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
12611262 )
12621263 defer unblock()
@@ -1476,7 +1477,7 @@ func TestAttachForwardsResize(t *testing.T) {
14761477 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
14771478 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
14781479 // The resize frame is forwarded via the sender goroutine.
1479- gotResize := make(chan ports.Resize, 1)
1480+ gotResize := make(chan protocol.Resize, 1)
14801481 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {
14811482 r, _ := ports.UnmarshalResize(f.Payload)
14821483 gotResize <- r
@@ -1739,16 +1740,16 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
17391740 term := newRunTerminal()
17401741 defer term.in.unblock()
17411742 lifecycle := domain.SessionLifecycleID{7, 8, 9}
1742- target := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
1743+ target := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
17431744 welcome := func(token uint64) ports.Frame {
17441745 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
17451746 SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: ports.CapabilityResume,
1746- CommittedIdentity: &ports.CommittedRouteIdentity{Target: target},
1747+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: target},
17471748 }))
17481749 }
17491750 tr1 := &recordingTransport{recvs: []recvItem{
17501751 {f: welcome(11)},
1751- {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(ports.RoutePosition{Target: target, ActiveTabID: "tab-2"}))},
1752+ {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(protocol.RoutePosition{Target: target, ActiveTabID: "tab-2"}))},
17521753 {err: io.EOF},
17531754 }}
17541755 tr2 := &recordingTransport{recvs: []recvItem{
@@ -1760,7 +1761,7 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
17601761 result := make(chan error, 1)
17611762 go func() {
17621763 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
1763- Intent: ports.IntentAttach, SessionName: "work", Origin: ports.RouteOriginLocal, OriginKey: "local",
1764+ Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",
17641765 })
17651766 }()
17661767 clock.fireReconnect(t)
@@ -1782,15 +1783,15 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
17821783 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
17831784 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
17841785 SessionID: name, SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume,
1785- CommittedIdentity: &ports.CommittedRouteIdentity{
1786- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1786+ CommittedIdentity: &protocol.CommittedRouteIdentity{
1787+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
17871788 },
17881789 }))
17891790 }
17901791 tr1 := &recordingTransport{recvs: []recvItem{
17911792 {f: welcome("old", oldLifecycle, 11)},
1792- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{
1793- Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
1793+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1794+ Target: protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
17941795 }))},
17951796 {err: io.EOF},
17961797 }}
@@ -1804,7 +1805,7 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
18041805 go func() {
18051806 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
18061807 Intent: ports.IntentAttach, SessionName: "old", Remote: true,
1807- Origin: ports.RouteOriginDiscovery, OriginKey: "remote",
1808+ Origin: protocol.RouteOriginDiscovery, OriginKey: "remote",
18081809 RemoteTarget: remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
18091810 })
18101811 }()
@@ -1814,7 +1815,7 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
18141815 reconnectHello := helloFromSend(t, tr2)
18151816 require.Equal(t, ports.IntentResume, reconnectHello.Intent)
18161817 require.Equal(t, "new", reconnectHello.Name)
1817- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)
1818+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)
18181819 require.Nil(t, reconnectHello.RemoteTarget)
18191820 }
18201821
M internal/usecase/client/clipboard_intercept_internal_test.go +5 -4
@@ -9,6 +9,7 @@ import (
99 "github.com/stretchr/testify/require"
1010
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 // ciFakeReader is a directly-controllable ports.ClipboardReader for
@@ -26,17 +27,17 @@ func (r *ciFakeReader) ReadImage(context.Context) (string, []byte, error) {
2627 return r.mime, r.data, r.err
2728 }
2829
29-func newTestClipboardIntercept(reader ports.ClipboardReader) (*clipboardIntercept, *pasteCoalescer, *pcFakeClock, *pcCollector, *[]ports.ImagePush) {
30+func newTestClipboardIntercept(reader ports.ClipboardReader) (*clipboardIntercept, *pasteCoalescer, *pcFakeClock, *pcCollector, *[]protocol.ImagePush) {
3031 clk := &pcFakeClock{}
3132 col := &pcCollector{}
3233 pc := newPasteCoalescer(clk, col.emit)
33- var images []ports.ImagePush
34+ var images []protocol.ImagePush
3435 ci := &clipboardIntercept{
3536 coalescer: pc,
3637 reader: reader,
3738 log: slog.New(slog.NewTextHandler(nil, &slog.HandlerOptions{Level: slog.LevelError + 1})),
3839 sendImage: func(mime string, data []byte) {
39- images = append(images, ports.ImagePush{Mime: mime, Data: data})
40+ images = append(images, protocol.ImagePush{Mime: mime, Data: data})
4041 },
4142 next: pc.Scan,
4243 }
@@ -53,7 +54,7 @@ func TestClipboardInterceptCtrlVWithImageSendsNoPassthrough(t *testing.T) {
5354 ci.Scan([]byte("b"))
5455
5556 require.Equal(t, 1, reader.n, "clipboard must be read exactly once per Ctrl+V")
56- require.Equal(t, []ports.ImagePush{{Mime: "image/png", Data: []byte("PNGDATA")}}, *images)
57+ require.Equal(t, []protocol.ImagePush{{Mime: "image/png", Data: []byte("PNGDATA")}}, *images)
5758 require.Equal(t, [][]byte{[]byte("a"), []byte("b")}, col.snapshot(), "0x16 itself must never reach the coalescer/pane")
5859 }
5960
M internal/usecase/client/clipboard_intercept_test.go +4 -3
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/client"
1819 )
1920
@@ -22,7 +23,7 @@ import (
2223 // transport that captures every MsgInput/MsgImagePush send, and a Recv that
2324 // answers Welcome then, once triggered, Detached — mirroring the pattern used
2425 // by the OSC-color-response test elsewhere in this package.
25-func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReader, stdin []byte, triggerByte byte) (gotInput chan []byte, gotImage chan ports.ImagePush) {
26+func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReader, stdin []byte, triggerByte byte) (gotInput chan []byte, gotImage chan protocol.ImagePush) {
2627 t.Helper()
2728 var out bytes.Buffer
2829 var restoreCount int32
@@ -39,7 +40,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
3940 tm.EXPECT().ResizeEvents().Return(resizeCh).Maybe()
4041
4142 gotInput = make(chan []byte, 8)
42- gotImage = make(chan ports.ImagePush, 1)
43+ gotImage = make(chan protocol.ImagePush, 1)
4344 allowDetach := make(chan struct{})
4445
4546 tr := portsmocks.NewMockTransport(t)
@@ -198,7 +199,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
198199 }
199200 }
200201 beforeOutput := out.String()
201- transport.recv <- frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))
202+ transport.recv <- frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))
202203 transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
203204
204205 select {
M internal/usecase/client/navigation_test.go +7 -6
@@ -5,6 +5,7 @@ import (
55
66 "github.com/bnema/vev/internal/domain"
77 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
89 "github.com/stretchr/testify/require"
910 )
1011
@@ -37,13 +38,13 @@ func TestValidateAttachRequestNavigationTable(t *testing.T) {
3738 valid bool
3839 }{
3940 {name: "ordinary route", request: AttachRequest{}, valid: true},
40- {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: ports.NavigationCapabilityHomePicker}, valid: false},
41- {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, NavigationCapabilities: ports.NavigationCapabilityHomePicker}, valid: true},
42- {name: "back on client-owned route", request: AttachRequest{StartupOverlay: ports.StartupOverlaySessionPicker, NavigationCapabilities: ports.NavigationCapabilityBack}, valid: true},
43- {name: "back on remote-target route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, StartupOverlay: ports.StartupOverlaySessionPicker, NavigationCapabilities: ports.NavigationCapabilityBack}, valid: false},
41+ {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
42+ {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: true},
43+ {name: "back on client-owned route", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: true},
44+ {name: "back on remote-target route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
4445 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},
45- {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: ports.NavigationCapabilityBack}, valid: false},
46- {name: "startup picker without back", request: AttachRequest{StartupOverlay: ports.StartupOverlaySessionPicker}, valid: false},
46+ {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
47+ {name: "startup picker without back", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
4748 }
4849
4950 for _, tt := range tests {
M internal/usecase/client/output_state.go +2 -2
@@ -1,6 +1,6 @@
11 package client
22
3-import "github.com/bnema/vev/internal/ports"
3+import "github.com/bnema/vev/internal/protocol"
44
55 // outputApplyState is the client-side dependency chain for one attachment.
66 // State-bearing frames are accepted only when their epoch, base, and view
@@ -12,7 +12,7 @@ type outputApplyState struct {
1212 initialized bool
1313 }
1414
15-func (s outputApplyState) next(output ports.Output) (outputApplyState, bool) {
15+func (s outputApplyState) next(output protocol.Output) (outputApplyState, bool) {
1616 if output.Epoch == 0 {
1717 return outputApplyState{}, false
1818 }
M internal/usecase/client/output_state_test.go +12 -12
@@ -4,28 +4,28 @@ import (
44 "testing"
55
66 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 )
99
1010 func TestOutputApplyStateTransitions(t *testing.T) {
11- full := ports.Output{Epoch: 1, Base: 0, New: 1, Full: true, ViewRevision: 2, Size: domain.Size{Cols: 80, Rows: 24}}
11+ full := protocol.Output{Epoch: 1, Base: 0, New: 1, Full: true, ViewRevision: 2, Size: domain.Size{Cols: 80, Rows: 24}}
1212 tests := []struct {
1313 name string
1414 state outputApplyState
15- output ports.Output
15+ output protocol.Output
1616 accepted bool
1717 want outputApplyState
1818 }{
1919 {name: "first full", output: full, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},
20- {name: "first gap", output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: false},
21- {name: "increment", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 1, state: 2, viewRevision: 2, initialized: true}},
22- {name: "base gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 0, New: 2, Full: true, ViewRevision: 2}, accepted: false},
23- {name: "revision gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 3}, accepted: false},
24- {name: "handoff side effect crosses newer epoch replay gate", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},
25- {name: "handoff side effect crosses stale epoch replay gate", state: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}, output: ports.Output{Epoch: 1, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}},
26- {name: "side effect cannot invent state", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Base: 1}, accepted: false},
27- {name: "side effect cannot claim full replay", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Full: true}, accepted: false},
28- {name: "new epoch reset", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Base: 0, New: 1, Full: true, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 2, state: 1, viewRevision: 3, initialized: true}},
20+ {name: "first gap", output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: false},
21+ {name: "increment", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 1, state: 2, viewRevision: 2, initialized: true}},
22+ {name: "base gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 0, New: 2, Full: true, ViewRevision: 2}, accepted: false},
23+ {name: "revision gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 3}, accepted: false},
24+ {name: "handoff side effect crosses newer epoch replay gate", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},
25+ {name: "handoff side effect crosses stale epoch replay gate", state: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}, output: protocol.Output{Epoch: 1, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}},
26+ {name: "side effect cannot invent state", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Base: 1}, accepted: false},
27+ {name: "side effect cannot claim full replay", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Full: true}, accepted: false},
28+ {name: "new epoch reset", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Base: 0, New: 1, Full: true, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 2, state: 1, viewRevision: 3, initialized: true}},
2929 }
3030 for _, tt := range tests {
3131 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/palette_attach_generation_test.go +9 -8
@@ -16,6 +16,7 @@ import (
1616 renderer "github.com/bnema/vev-vt"
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
1920 )
2021
2122 // attachPaletteClock deliberately exposes each independently-created timer.
@@ -115,8 +116,8 @@ func (*attachPaletteTerminalHarness) Flush() error { r
115116 type attachPaletteTransport struct {
116117 mu sync.Mutex
117118 frames []ports.Frame
118- themes []ports.Theme
119- themeCh chan ports.Theme
119+ themes []protocol.Theme
120+ themeCh chan protocol.Theme
120121 inputCh chan ports.Input
121122 detached chan ports.Frame
122123 themeSendErr error
@@ -125,7 +126,7 @@ type attachPaletteTransport struct {
125126
126127 func newAttachPaletteTransport() *attachPaletteTransport {
127128 return &attachPaletteTransport{
128- themeCh: make(chan ports.Theme, 16),
129+ themeCh: make(chan protocol.Theme, 16),
129130 inputCh: make(chan ports.Input, 16),
130131 detached: make(chan ports.Frame, 1),
131132 }
@@ -168,10 +169,10 @@ func (t *attachPaletteTransport) Recv() (ports.Frame, error) {
168169 return <-t.detached, nil
169170 }
170171 func (*attachPaletteTransport) Close() error { return nil }
171-func (t *attachPaletteTransport) snapshots() []ports.Theme {
172+func (t *attachPaletteTransport) snapshots() []protocol.Theme {
172173 t.mu.Lock()
173174 defer t.mu.Unlock()
174- return append([]ports.Theme(nil), t.themes...)
175+ return append([]protocol.Theme(nil), t.themes...)
175176 }
176177
177178 type attachPaletteHarness struct {
@@ -228,7 +229,7 @@ func (h *attachPaletteHarness) nextWrite(t *testing.T) string {
228229 t.Helper()
229230 return <-h.writer.wrote
230231 }
231-func (h *attachPaletteHarness) nextTheme(t *testing.T) ports.Theme {
232+func (h *attachPaletteHarness) nextTheme(t *testing.T) protocol.Theme {
232233 t.Helper()
233234 return <-h.transport.themeCh
234235 }
@@ -428,7 +429,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
428429 defer cancel()
429430
430431 coordinator := newPaletteGenerationCoordinator()
431- coordinator.start(ports.Theme{}, false)
432+ coordinator.start(protocol.Theme{}, false)
432433 var activeGeneration atomic.Uint64
433434 activeGeneration.Store(uint64(coordinator.current.id))
434435
@@ -466,7 +467,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
466467 // This models attachAttempt handling the scheme event. The replacement is
467468 // draining, so a marker tagged with its ID would incorrectly start its
468469 // color batch before the real drain boundary arrives.
469- coordinator.start(ports.Theme{SchemeKnown: true, Light: scheme.light}, true)
470+ coordinator.start(protocol.Theme{SchemeKnown: true, Light: scheme.light}, true)
470471 newID := coordinator.current.id
471472 activeGeneration.Store(uint64(newID))
472473 close(schemeHandled)
M internal/usecase/client/palette_attach_test.go +2 -1
@@ -14,6 +14,7 @@ import (
1414 renderer "github.com/bnema/vev-vt"
1515 "github.com/bnema/vev/internal/domain"
1616 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
1718 )
1819
1920 type paletteAttachClock struct{}
@@ -78,7 +79,7 @@ func (t *paletteAttachTerminal) Flush() error { return n
7879 type paletteAttachTransport struct {
7980 mu sync.Mutex
8081 sent []ports.Frame
81- themes []ports.Theme
82+ themes []protocol.Theme
8283 finals int
8384 finalSet chan struct{}
8485 }
M internal/usecase/client/palette_generation.go +7 -7
@@ -4,7 +4,7 @@ import (
44 "time"
55
66 renderer "github.com/bnema/vev-vt"
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 )
99
1010 // Palette queries always have a DECRQM boundary. The first marker drains
@@ -38,7 +38,7 @@ const (
3838 type paletteGeneration struct {
3939 id paletteGenerationID
4040 phase paletteGenerationPhase
41- theme ports.Theme
41+ theme protocol.Theme
4242 expectedMarkers int
4343 awaitingLateDrainBoundary bool
4444 }
@@ -83,7 +83,7 @@ type paletteGenerationAction struct {
8383 kind paletteGenerationActionKind
8484 id paletteGenerationID
8585 bytes string
86- theme ports.Theme
86+ theme protocol.Theme
8787 deadline time.Duration
8888 }
8989
@@ -93,7 +93,7 @@ type paletteGenerationCoordinator struct {
9393 next paletteGenerationID
9494 current paletteGeneration
9595 active bool
96- finalized ports.Theme
96+ finalized protocol.Theme
9797 }
9898
9999 func newPaletteGenerationCoordinator() *paletteGenerationCoordinator {
@@ -104,7 +104,7 @@ func newPaletteGenerationCoordinator() *paletteGenerationCoordinator {
104104 // batch or a replacement drain. The retained foreground/background and
105105 // capability avoid a full neutral flash, while the accumulator begins with no
106106 // defaults or palette values so a definitive result never mixes generations.
107-func (c *paletteGenerationCoordinator) start(retained ports.Theme, replacement bool) []paletteGenerationAction {
107+func (c *paletteGenerationCoordinator) start(retained protocol.Theme, replacement bool) []paletteGenerationAction {
108108 actions := c.cancelCurrent()
109109 c.next++
110110 id := c.next
@@ -115,7 +115,7 @@ func (c *paletteGenerationCoordinator) start(retained ports.Theme, replacement b
115115 c.current = paletteGeneration{
116116 id: id,
117117 phase: generationCollecting,
118- theme: ports.Theme{
118+ theme: protocol.Theme{
119119 TrueColor: retained.TrueColor,
120120 SchemeKnown: retained.SchemeKnown,
121121 Light: retained.Light,
@@ -250,7 +250,7 @@ func (c *paletteGenerationCoordinator) palette(id paletteGenerationID, slot uint
250250 c.handle(paletteGenerationEvent{id: id, kind: paletteEventPalette, slot: slot, rgb: rgb})
251251 }
252252
253-func (c *paletteGenerationCoordinator) finalizedTheme() ports.Theme { return c.finalized }
253+func (c *paletteGenerationCoordinator) finalizedTheme() protocol.Theme { return c.finalized }
254254
255255 // paletteMarkerScanner consumes only complete DECRQM 2031 replies and retains
256256 // possible prefixes across reads, forwarding all ordinary input exactly once.
M internal/usecase/client/palette_generation_test.go +3 -3
@@ -7,7 +7,7 @@ import (
77 "github.com/stretchr/testify/require"
88
99 renderer "github.com/bnema/vev-vt"
10- "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1111 )
1212
1313 // manualPaletteClock applies only declarative coordinator timer actions. Its
@@ -52,8 +52,8 @@ func (c *manualPaletteClock) fireCompletion(g *paletteGenerationCoordinator, id
5252 return g.handle(paletteGenerationEvent{id: id, kind: paletteEventCompletionDeadline})
5353 }
5454
55-func generationTheme() ports.Theme {
56- return ports.Theme{
55+func generationTheme() protocol.Theme {
56+ return protocol.Theme{
5757 TrueColor: true,
5858 HasForeground: true,
5959 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
M internal/usecase/client/reconnect_toast_test.go +14 -13
@@ -20,6 +20,7 @@ import (
2020 "github.com/bnema/vev/internal/domain"
2121 "github.com/bnema/vev/internal/ports"
2222 portsmocks "github.com/bnema/vev/internal/ports/mocks"
23+ "github.com/bnema/vev/internal/protocol"
2324 )
2425
2526 type reconnectToastTerminalHarness struct {
@@ -393,7 +394,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
393394 require.Contains(t, firstToast, "┌")
394395 require.Contains(t, firstToast, "probing UDP path")
395396
396- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
397+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
397398 Epoch: 1,
398399 Base: 0,
399400 New: 2,
@@ -412,7 +413,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
412413
413414 beforeAwaitingReset := out.String()
414415 beforeStatelessFlushes := flushes.Load()
415- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
416+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
416417 Epoch: 1,
417418 Base: 0,
418419 New: 0,
@@ -424,7 +425,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
424425 }, time.Second, time.Millisecond)
425426
426427 afterStateless := out.String()
427- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
428+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
428429 Epoch: 1,
429430 Base: 2,
430431 New: 3,
@@ -435,7 +436,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
435436 require.Contains(t, out.String(), "intervening incremental")
436437 require.Contains(t, afterStateless[len(beforeAwaitingReset):], "stateless side effect")
437438
438- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
439+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
439440 Epoch: 2,
440441 Base: 0,
441442 New: 4,
@@ -448,7 +449,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
448449 require.NotContains(t, out.String()[len(beforeAwaitingReset):], strings.Repeat(" ", reconnectToastBoundsFor(term.size, "probing UDP path").Width))
449450
450451 beforeIncrementFlushes := flushes.Load()
451- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
452+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
452453 Epoch: 2,
453454 Base: 4,
454455 New: 5,
@@ -491,7 +492,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
491492 require.NotContains(t, out.String(), offlineMessage)
492493
493494 beforeReset := out.String()
494- for _, output := range []ports.Output{
495+ for _, output := range []protocol.Output{
495496 {Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("first skipped increment")},
496497 {Epoch: 1, Base: 2, New: 3, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("second skipped increment")},
497498 } {
@@ -507,7 +508,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
507508 // Handoff cleanup is an ordered terminal side effect, not a replay state.
508509 // It must cross the outstanding reset gate, flush before the control handoff,
509510 // and must not manufacture an independent ACK.
510- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
511+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
511512 Epoch: 2,
512513 Size: domain.Size{Cols: 1, Rows: 1},
513514 Data: []byte("handoff graphics cleanup"),
@@ -518,7 +519,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
518519 require.False(t, sentAck, "state-independent cleanup must not be ACKed")
519520
520521 beforeResetFlushes := flushes.Load()
521- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
522+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
522523 Epoch: 2,
523524 Base: 0,
524525 New: 4,
@@ -533,7 +534,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
533534 requireAckedState(t, tr.sends, 2, 4)
534535
535536 beforeIncrementFlushes := flushes.Load()
536- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
537+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
537538 Epoch: 2,
538539 Base: 4,
539540 New: 5,
@@ -579,7 +580,7 @@ func requireSentFrame(t *testing.T, sends *reconnectToastSentFrames, description
579580 }
580581 }
581582
582-func requireResize(t *testing.T, sends *reconnectToastSentFrames) ports.Resize {
583+func requireResize(t *testing.T, sends *reconnectToastSentFrames) protocol.Resize {
583584 t.Helper()
584585 frame := requireSentFrame(t, sends, "Resize frame", func(frame ports.Frame) bool {
585586 return frame.Type == ports.MsgResize
@@ -706,7 +707,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
706707 require.Eventually(t, func() bool { return flushes.Load() > beforeStatus }, time.Second, time.Millisecond)
707708 beforeOutput := flushes.Load()
708709
709- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
710+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
710711 Epoch: 1,
711712 Base: 0,
712713 New: 2,
@@ -758,7 +759,7 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
758759 }
759760
760761 const output = "local status output remains visible"
761- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
762+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
762763 Epoch: 1,
763764 Base: 0,
764765 New: 2,
@@ -826,7 +827,7 @@ func TestReconnectOverlayRedrawFailureDoesNotObserveOrAckOutput(t *testing.T) {
826827 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
827828 requireReconnectToastOutput(t, out.completed)
828829 arm()
829- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{
830+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
830831 Epoch: 1,
831832 Base: 0,
832833 New: 2,
M internal/usecase/client/remote_attach_validation_test.go +2 -1
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
1111 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 func TestMalformedAttachRequestsNeverDial(t *testing.T) {
@@ -19,7 +20,7 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
1920 {name: "missing session name", request: AttachRequest{Intent: ports.IntentAttach}},
2021 {name: "unsafe session name", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "bad name"}},
2122 {name: "daemon-owned without remote", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "main", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}},
22- {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: ports.NavigationCapabilityHomePicker}},
23+ {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},
2324 }
2425 for _, tt := range tests {
2526 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/routes.go +58 -57
@@ -8,6 +8,7 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 // maxRouteLedgerEntries is the product history cap. It is deliberately
@@ -36,11 +37,11 @@ type routeIdentity struct {
3637
3738 func (i routeIdentity) empty() bool { return i.key == 0 && i.generation == 0 }
3839
39-func (i routeIdentity) wire() ports.RouteRef {
40- return ports.RouteRef{Key: uint64(i.key), Generation: uint64(i.generation)}
40+func (i routeIdentity) wire() protocol.RouteRef {
41+ return protocol.RouteRef{Key: uint64(i.key), Generation: uint64(i.generation)}
4142 }
4243
43-func identityFromWire(ref ports.RouteRef) routeIdentity {
44+func identityFromWire(ref protocol.RouteRef) routeIdentity {
4445 return routeIdentity{key: routeKey(ref.Key), generation: routeGeneration(ref.Generation)}
4546 }
4647
@@ -50,16 +51,16 @@ func identityFromWire(ref ports.RouteRef) routeIdentity {
5051 type routePresentation struct {
5152 name string
5253 hostLabel string
53- kind ports.RouteKind
54+ kind protocol.RouteKind
5455 ephemeral bool
5556 attention bool
56- reachability ports.RouteReachability
57+ reachability protocol.RouteReachability
5758 }
5859
5960 type routeCandidate struct {
60- origin ports.RouteOrigin
61+ origin protocol.RouteOrigin
6162 originKey string
62- target ports.ExactSessionTarget
63+ target protocol.ExactSessionTarget
6364 presentation routePresentation
6465 dialer ports.Dialer
6566 request AttachRequest
@@ -69,9 +70,9 @@ type routeCandidate struct {
6970
7071 type routeRecord struct {
7172 identity routeIdentity
72- origin ports.RouteOrigin
73+ origin protocol.RouteOrigin
7374 originKey string
74- target ports.ExactSessionTarget
75+ target protocol.ExactSessionTarget
7576 presentation routePresentation
7677 dialer ports.Dialer
7778 request AttachRequest
@@ -98,7 +99,7 @@ type routeLedger struct {
9899
99100 func newRouteLedger() *routeLedger { return &routeLedger{} }
100101
101-func cloneCommittedIdentity(identity *ports.CommittedRouteIdentity) *ports.CommittedRouteIdentity {
102+func cloneCommittedIdentity(identity *protocol.CommittedRouteIdentity) *protocol.CommittedRouteIdentity {
102103 if identity == nil {
103104 return nil
104105 }
@@ -106,7 +107,7 @@ func cloneCommittedIdentity(identity *ports.CommittedRouteIdentity) *ports.Commi
106107 return &clone
107108 }
108109
109-func cloneRoutePosition(position *ports.RoutePosition) *ports.RoutePosition {
110+func cloneRoutePosition(position *protocol.RoutePosition) *protocol.RoutePosition {
110111 if position == nil {
111112 return nil
112113 }
@@ -114,13 +115,13 @@ func cloneRoutePosition(position *ports.RoutePosition) *ports.RoutePosition {
114115 return &clone
115116 }
116117
117-func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {
118+func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {
118119 origin := normalizeRouteOrigin(request.Origin, request.Remote)
119120 originKey := normalizeRouteOriginKey(request.OriginKey, origin)
120- kind := ports.RouteKindLocal
121+ kind := protocol.RouteKindLocal
121122 hostLabel := ""
122- if request.Remote || request.RemoteTarget != nil || origin != ports.RouteOriginLocal {
123- kind = ports.RouteKindRemote
123+ if request.Remote || request.RemoteTarget != nil || origin != protocol.RouteOriginLocal {
124+ kind = protocol.RouteKindRemote
124125 if request.RemoteTarget != nil {
125126 hostLabel = domain.RemoteDisplayOrigin(request.RemoteTarget.DisplayOrigin)
126127 }
@@ -150,7 +151,7 @@ func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRout
150151 hostLabel: hostLabel,
151152 kind: kind,
152153 ephemeral: identity.Ephemeral,
153- reachability: ports.RouteReachabilityReachable,
154+ reachability: protocol.RouteReachabilityReachable,
154155 },
155156 dialer: dialer,
156157 request: request,
@@ -158,7 +159,7 @@ func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRout
158159 }
159160 }
160161
161-func routeCandidateForCommittedIdentity(active routeRecord, identity ports.CommittedRouteIdentity) routeCandidate {
162+func routeCandidateForCommittedIdentity(active routeRecord, identity protocol.CommittedRouteIdentity) routeCandidate {
162163 request := cloneAttachRequest(active.request)
163164 request.SessionName = identity.Target.SessionName
164165 request.ExactTarget = &identity.Target
@@ -175,7 +176,7 @@ func routeCandidateForCommittedIdentity(active routeRecord, identity ports.Commi
175176 hostLabel: active.presentation.hostLabel,
176177 kind: active.presentation.kind,
177178 ephemeral: identity.Ephemeral,
178- reachability: ports.RouteReachabilityReachable,
179+ reachability: protocol.RouteReachabilityReachable,
179180 },
180181 dialer: active.dialer,
181182 request: request,
@@ -184,38 +185,38 @@ func routeCandidateForCommittedIdentity(active routeRecord, identity ports.Commi
184185 }
185186 }
186187
187-func normalizeRouteOrigin(origin ports.RouteOrigin, remote bool) ports.RouteOrigin {
188+func normalizeRouteOrigin(origin protocol.RouteOrigin, remote bool) protocol.RouteOrigin {
188189 if origin != 0 {
189190 return origin
190191 }
191192 if remote {
192- return ports.RouteOriginRemote
193+ return protocol.RouteOriginRemote
193194 }
194- return ports.RouteOriginLocal
195+ return protocol.RouteOriginLocal
195196 }
196197
197-func routeFailureCode(err error) ports.RouteFailureCode {
198+func routeFailureCode(err error) protocol.RouteFailureCode {
198199 var protocolErr *ProtocolError
199200 if errors.As(err, &protocolErr) {
200201 switch protocolErr.Code {
201202 case ports.ErrNoSuchSession, ports.ErrNoSuchTarget:
202- return ports.RouteFailureNoSuchRoute
203+ return protocol.RouteFailureNoSuchRoute
203204 }
204205 }
205206 if errors.Is(err, errRouteTargetChanged) {
206- return ports.RouteFailureTargetChanged
207+ return protocol.RouteFailureTargetChanged
207208 }
208- return ports.RouteFailureUnavailable
209+ return protocol.RouteFailureUnavailable
209210 }
210211
211-func normalizeRouteOriginKey(key string, origin ports.RouteOrigin) string {
212+func normalizeRouteOriginKey(key string, origin protocol.RouteOrigin) string {
212213 if key != "" {
213214 return key
214215 }
215216 switch origin {
216- case ports.RouteOriginLocal:
217+ case protocol.RouteOriginLocal:
217218 return "local"
218- case ports.RouteOriginDiscovery:
219+ case protocol.RouteOriginDiscovery:
219220 return "discovery"
220221 default:
221222 return "remote"
@@ -223,10 +224,10 @@ func normalizeRouteOriginKey(key string, origin ports.RouteOrigin) string {
223224 }
224225
225226 func validateRoutePresentation(p routePresentation) error {
226- if err := ports.ValidateRouteLabel(p.name, false); err != nil {
227+ if err := protocol.ValidateRouteLabel(p.name, false); err != nil {
227228 return err
228229 }
229- if err := ports.ValidateRouteLabel(p.hostLabel, true); err != nil {
230+ if err := protocol.ValidateRouteLabel(p.hostLabel, true); err != nil {
230231 return err
231232 }
232233 if err := p.kind.Validate(); err != nil {
@@ -247,7 +248,7 @@ func (l *routeLedger) nextIdentityLocked() (routeIdentity, error) {
247248 return routeIdentity{key: l.nextKey, generation: l.generation}, nil
248249 }
249250
250-func (l *routeLedger) findByTargetLocked(origin ports.RouteOrigin, originKey string, target ports.ExactSessionTarget) int {
251+func (l *routeLedger) findByTargetLocked(origin protocol.RouteOrigin, originKey string, target protocol.ExactSessionTarget) int {
251252 for i := range l.entries {
252253 entry := l.entries[i]
253254 if entry.origin == origin && entry.originKey == originKey && entry.target == target {
@@ -340,7 +341,7 @@ func (l *routeLedger) commitAttach(candidate routeCandidate) (routeIdentity, err
340341 // commitCommittedIdentity snapshots the current active record and commits the
341342 // daemon's local transition under one lock. The active route, origin, dialer,
342343 // and attach request cannot be read from different ledger generations.
343-func (l *routeLedger) commitCommittedIdentity(identity ports.CommittedRouteIdentity) (routeIdentity, error) {
344+func (l *routeLedger) commitCommittedIdentity(identity protocol.CommittedRouteIdentity) (routeIdentity, error) {
344345 if err := identity.Validate(); err != nil {
345346 return routeIdentity{}, err
346347 }
@@ -383,7 +384,7 @@ func (l *routeLedger) commitCommittedIdentity(identity ports.CommittedRouteIdent
383384 // updateRoutePosition remembers a cursor only for the currently active exact
384385 // route. Delayed frames from a prior transition are valid wire messages but do
385386 // not mutate another route.
386-func (l *routeLedger) updateRoutePosition(position ports.RoutePosition) error {
387+func (l *routeLedger) updateRoutePosition(position protocol.RoutePosition) error {
387388 if err := position.Validate(); err != nil {
388389 return err
389390 }
@@ -459,7 +460,7 @@ func (l *routeLedger) indexByIdentityLocked(identity routeIdentity) int {
459460 return -1
460461 }
461462
462-func (l *routeLedger) lookup(ref ports.RouteRef) (routeRecord, bool) {
463+func (l *routeLedger) lookup(ref protocol.RouteRef) (routeRecord, bool) {
463464 identity := identityFromWire(ref)
464465 if identity.empty() {
465466 return routeRecord{}, false
@@ -481,21 +482,21 @@ func cloneRouteRecord(entry routeRecord) routeRecord {
481482
482483 // attentionSubscription publishes only routes served by the active route's
483484 // daemon. Other route origins remain private to their owning daemon.
484-func (l *routeLedger) attentionSubscription() ports.RouteAttentionSubscription {
485+func (l *routeLedger) attentionSubscription() protocol.RouteAttentionSubscription {
485486 l.mu.RLock()
486487 defer l.mu.RUnlock()
487488
488489 activeIndex := l.indexByIdentityLocked(l.active)
489490 if activeIndex < 0 {
490- return ports.RouteAttentionSubscription{}
491+ return protocol.RouteAttentionSubscription{}
491492 }
492493 active := l.entries[activeIndex]
493- subscription := ports.RouteAttentionSubscription{Targets: make([]ports.RouteAttentionTarget, 0, len(l.entries))}
494+ subscription := protocol.RouteAttentionSubscription{Targets: make([]protocol.RouteAttentionTarget, 0, len(l.entries))}
494495 for _, entry := range l.entries {
495496 if entry.identity == active.identity || entry.origin != active.origin || entry.originKey != active.originKey {
496497 continue
497498 }
498- subscription.Targets = append(subscription.Targets, ports.RouteAttentionTarget{
499+ subscription.Targets = append(subscription.Targets, protocol.RouteAttentionTarget{
499500 Ref: entry.identity.wire(),
500501 Target: entry.target,
501502 })
@@ -503,8 +504,8 @@ func (l *routeLedger) attentionSubscription() ports.RouteAttentionSubscription {
503504 return subscription
504505 }
505506
506-func (r routeRecord) snapshotEntry() ports.RecentRouteEntry {
507- return ports.RecentRouteEntry{
507+func (r routeRecord) snapshotEntry() protocol.RecentRouteEntry {
508+ return protocol.RecentRouteEntry{
508509 Key: uint64(r.identity.key),
509510 Generation: uint64(r.identity.generation),
510511 Target: r.target,
@@ -517,11 +518,11 @@ func (r routeRecord) snapshotEntry() ports.RecentRouteEntry {
517518 }
518519 }
519520
520-func (l *routeLedger) snapshot() ports.RecentRouteSnapshot {
521+func (l *routeLedger) snapshot() protocol.RecentRouteSnapshot {
521522 l.mu.RLock()
522523 defer l.mu.RUnlock()
523524
524- snapshot := ports.RecentRouteSnapshot{
525+ snapshot := protocol.RecentRouteSnapshot{
525526 Generation: uint64(l.generation),
526527 Active: l.active.wire(),
527528 Previous: l.previous.wire(),
@@ -530,7 +531,7 @@ func (l *routeLedger) snapshot() ports.RecentRouteSnapshot {
530531 if len(l.entries) == 0 {
531532 return snapshot
532533 }
533- snapshot.Entries = make([]ports.RecentRouteEntry, 0, len(l.entries))
534+ snapshot.Entries = make([]protocol.RecentRouteEntry, 0, len(l.entries))
534535 for _, entry := range l.entries {
535536 if entry.identity == l.active {
536537 snapshot.ActiveEntry = entry.snapshotEntry()
@@ -548,7 +549,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
548549 origin := normalizeRouteOrigin(active.Origin, active.Remote)
549550 originKey := normalizeRouteOriginKey(active.OriginKey, origin)
550551 environmentPolicy := target.EnvironmentPolicy
551- if origin == ports.RouteOriginLocal {
552+ if origin == protocol.RouteOriginLocal {
552553 environmentPolicy = ports.EnvironmentPolicyClientOwned
553554 }
554555
@@ -569,7 +570,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
569570 request.RemoteTarget = nil
570571 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
571572 request.NavigationCapabilities = 0
572- request.StartupOverlay = ports.StartupOverlayNone
573+ request.StartupOverlay = protocol.StartupOverlayNone
573574 }
574575 request.EnvironmentPolicy = environmentPolicy
575576 request.ExactTarget = target.ExactTarget
@@ -587,25 +588,25 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
587588 request.RemoteTarget = nil
588589 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
589590 request.NavigationCapabilities = 0
590- request.StartupOverlay = ports.StartupOverlayNone
591+ request.StartupOverlay = protocol.StartupOverlayNone
591592 }
592593 request.EnvironmentPolicy = environmentPolicy
593594 return request
594595 }
595596
596-func (l *routeLedger) activeRef() ports.RouteRef {
597+func (l *routeLedger) activeRef() protocol.RouteRef {
597598 l.mu.RLock()
598599 defer l.mu.RUnlock()
599600 return l.active.wire()
600601 }
601602
602-func (l *routeLedger) previousRef() ports.RouteRef {
603+func (l *routeLedger) previousRef() protocol.RouteRef {
603604 l.mu.RLock()
604605 defer l.mu.RUnlock()
605606 return l.previous.wire()
606607 }
607608
608-func (l *routeLedger) homeRef() ports.RouteRef {
609+func (l *routeLedger) homeRef() protocol.RouteRef {
609610 l.mu.RLock()
610611 defer l.mu.RUnlock()
611612 return l.home.wire()
@@ -619,13 +620,13 @@ type routeTransitionPlan struct {
619620 record routeRecord
620621 snapshotGeneration routeGeneration
621622 active routeIdentity
622- origin ports.RouteOrigin
623+ origin protocol.RouteOrigin
623624 originKey string
624- target ports.ExactSessionTarget
625+ target protocol.ExactSessionTarget
625626 }
626627
627628 type routeTransitionCommit struct {
628- identity ports.CommittedRouteIdentity
629+ identity protocol.CommittedRouteIdentity
629630 presentation routePresentation
630631 dialer ports.Dialer
631632 resumeToken uint64
@@ -650,11 +651,11 @@ type routeNavigationSelection struct {
650651 // generation, and active identity under one read lock. The commit phase uses
651652 // the captured generation and active identity to reject interleavings that
652653 // changed the ledger while dialing.
653-func (l *routeLedger) navigationSelection(action ports.RouteNavigationAction) (routeNavigationSelection, bool) {
654+func (l *routeLedger) navigationSelection(action protocol.RouteNavigationAction) (routeNavigationSelection, bool) {
654655 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
655656 return routeNavigationSelection{}, false
656657 }
657- identity := identityFromWire(ports.RouteRef{Key: action.Key, Generation: action.Generation})
658+ identity := identityFromWire(protocol.RouteRef{Key: action.Key, Generation: action.Generation})
658659 l.mu.RLock()
659660 defer l.mu.RUnlock()
660661 if uint64(l.generation) != action.SnapshotGeneration {
@@ -680,7 +681,7 @@ func (l *routeLedger) navigationSelection(action ports.RouteNavigationAction) (r
680681
681682 // navigationRecord validates that an action belongs to the latest complete
682683 // snapshot. The active route is metadata-only and selecting it is a no-op.
683-func (l *routeLedger) navigationRecord(action ports.RouteNavigationAction) (routeRecord, bool, bool) {
684+func (l *routeLedger) navigationRecord(action protocol.RouteNavigationAction) (routeRecord, bool, bool) {
684685 selection, ok := l.navigationSelection(action)
685686 return selection.selected, ok, selection.noOp
686687 }
@@ -688,7 +689,7 @@ func (l *routeLedger) navigationRecord(action ports.RouteNavigationAction) (rout
688689 // navigate resolves a captured key/generation, attempts resume before exact
689690 // attach, and commits only after the connector returns a matching identity.
690691 // A failed transition restores the prior live route and leaves history alone.
691-func (l *routeLedger) navigate(ctx context.Context, action ports.RouteNavigationAction, connector routeTransitionConnector) error {
692+func (l *routeLedger) navigate(ctx context.Context, action protocol.RouteNavigationAction, connector routeTransitionConnector) error {
692693 selection, ok := l.navigationSelection(action)
693694 if !ok {
694695 return errRouteStaleSelection
@@ -704,7 +705,7 @@ func (l *routeLedger) navigate(ctx context.Context, action ports.RouteNavigation
704705 if ctx == nil {
705706 return errors.New("route transition context is nil")
706707 }
707- selected := ports.RouteRef{Key: action.Key, Generation: action.Generation}
708+ selected := protocol.RouteRef{Key: action.Key, Generation: action.Generation}
708709 plan := routeTransitionPlan{
709710 identity: identityFromWire(selected),
710711 record: record,
M internal/usecase/client/routes_test.go +66 -65
@@ -8,22 +8,23 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 "github.com/stretchr/testify/require"
1213 )
1314
14-func routeTestTarget(index byte) ports.ExactSessionTarget {
15- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{index + 1}, SessionName: "session-" + string(rune('a'+index%26)) + string(rune('0'+index/26))}
15+func routeTestTarget(index byte) protocol.ExactSessionTarget {
16+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{index + 1}, SessionName: "session-" + string(rune('a'+index%26)) + string(rune('0'+index/26))}
1617 }
1718
18-func routeTestCandidate(index byte, origin ports.RouteOrigin) routeCandidate {
19+func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate {
1920 target := routeTestTarget(index)
2021 return routeCandidate{
2122 origin: origin,
2223 target: target,
2324 presentation: routePresentation{
2425 name: target.SessionName,
25- kind: ports.RouteKindLocal,
26- reachability: ports.RouteReachabilityReachable,
26+ kind: protocol.RouteKindLocal,
27+ reachability: protocol.RouteReachabilityReachable,
2728 },
2829 request: AttachRequest{
2930 Intent: ports.IntentAttach,
@@ -37,8 +38,8 @@ func TestRouteCandidateRetainsHostLabelWithoutDiscoveryTarget(t *testing.T) {
3738 target := routeTestTarget(1)
3839 candidate := routeCandidateForAttach(AttachRequest{
3940 Intent: ports.IntentAttach, SessionName: target.SessionName, Remote: true,
40- Origin: ports.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",
41- }, ports.CommittedRouteIdentity{Target: target}, nil, 0)
41+ Origin: protocol.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",
42+ }, protocol.CommittedRouteIdentity{Target: target}, nil, 0)
4243
4344 require.Nil(t, candidate.request.RemoteTarget)
4445 require.Equal(t, "remote", candidate.request.HostLabel)
@@ -49,7 +50,7 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
4950 ledger := newRouteLedger()
5051 var keys []routeKey
5152 for i := byte(0); i < maxRouteLedgerEntries+8; i++ {
52- candidate := routeTestCandidate(i, ports.RouteOriginLocal)
53+ candidate := routeTestCandidate(i, protocol.RouteOriginLocal)
5354 candidate.home = i == 0
5455 identity, err := ledger.commit(candidate)
5556 require.NoError(t, err)
@@ -61,11 +62,11 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
6162 require.Len(t, snapshot.Entries, maxRouteLedgerEntries-1)
6263 require.Equal(t, uint64(keys[len(keys)-1]), snapshot.Active.Key)
6364 require.Equal(t, routeTestTarget(maxRouteLedgerEntries+7), snapshot.ActiveEntry.Target)
64- require.Equal(t, snapshot.Active, ports.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation})
65+ require.Equal(t, snapshot.Active, protocol.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation})
6566 for _, entry := range snapshot.Entries {
6667 require.NoError(t, entry.Target.Validate())
6768 require.Equal(t, entry.Name, entry.Target.SessionName)
68- require.NotEqual(t, snapshot.Active, ports.RouteRef{Key: entry.Key, Generation: entry.Generation})
69+ require.NotEqual(t, snapshot.Active, protocol.RouteRef{Key: entry.Key, Generation: entry.Generation})
6970 }
7071 require.Equal(t, uint64(keys[len(keys)-2]), snapshot.Previous.Key)
7172 require.Equal(t, uint64(keys[0]), snapshot.Home.Key, "home is retained when older entries are evicted")
@@ -79,7 +80,7 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
7980 require.Equal(t, originalName, fresh.Entries[0].Name)
8081
8182 old := ledger.activeRef()
82- _, err := ledger.commit(routeTestCandidate(maxRouteLedgerEntries+9, ports.RouteOriginLocal))
83+ _, err := ledger.commit(routeTestCandidate(maxRouteLedgerEntries+9, protocol.RouteOriginLocal))
8384 require.NoError(t, err)
8485 require.NotEqual(t, old, ledger.activeRef())
8586 require.Equal(t, old, ledger.previousRef(), "the old active route becomes the previous route")
@@ -87,21 +88,21 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
8788
8889 func TestRouteLedgerRemembersTabsIndependentlyPerExactRoute(t *testing.T) {
8990 ledger := newRouteLedger()
90- first := routeTestCandidate(1, ports.RouteOriginLocal)
91+ first := routeTestCandidate(1, protocol.RouteOriginLocal)
9192 firstIdentity, err := ledger.commit(first)
9293 require.NoError(t, err)
9394 firstRef := ledger.activeRef()
9495 firstSnapshotGeneration := ledger.snapshot().Generation
95- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
96+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
9697 require.Equal(t, firstRef, ledger.activeRef(), "tab memory must not rotate route identity")
9798 require.Equal(t, firstSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")
9899
99- second := routeTestCandidate(2, ports.RouteOriginRemote)
100+ second := routeTestCandidate(2, protocol.RouteOriginRemote)
100101 secondIdentity, err := ledger.commit(second)
101102 require.NoError(t, err)
102103 secondRef := ledger.activeRef()
103104 secondSnapshotGeneration := ledger.snapshot().Generation
104- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: second.target, ActiveTabID: "tab-second"}))
105+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: second.target, ActiveTabID: "tab-second"}))
105106 require.Equal(t, secondRef, ledger.activeRef(), "tab memory must not rotate route identity")
106107 require.Equal(t, secondSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")
107108
@@ -115,13 +116,13 @@ func TestRouteLedgerRemembersTabsIndependentlyPerExactRoute(t *testing.T) {
115116
116117 func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
117118 ledger := newRouteLedger()
118- first := routeTestCandidate(1, ports.RouteOriginLocal)
119+ first := routeTestCandidate(1, protocol.RouteOriginLocal)
119120 first.originKey = "local"
120121 _, err := ledger.commit(first)
121122 require.NoError(t, err)
122- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
123+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
123124
124- second := routeTestCandidate(2, ports.RouteOriginLocal)
125+ second := routeTestCandidate(2, protocol.RouteOriginLocal)
125126 second.originKey = "local"
126127 _, err = ledger.commit(second)
127128 require.NoError(t, err)
@@ -130,8 +131,8 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
130131 recreated.LifecycleID[0]++
131132 for _, tt := range []struct {
132133 name string
133- target ports.ExactSessionTarget
134- wantExactTarget *ports.ExactSessionTarget
134+ target protocol.ExactSessionTarget
135+ wantExactTarget *protocol.ExactSessionTarget
135136 wantTab domain.TabStableID
136137 }{
137138 {name: "matching lifecycle restores remembered tab", target: first.target, wantExactTarget: &first.target, wantTab: "tab-first"},
@@ -153,11 +154,11 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
153154 }
154155
155156 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {
156- active := routeTestCandidate(2, ports.RouteOriginLocal)
157+ active := routeTestCandidate(2, protocol.RouteOriginLocal)
157158 active.originKey = "local"
158159 active.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
159- active.request.NavigationCapabilities = ports.NavigationCapabilityHomePicker
160- active.request.StartupOverlay = ports.StartupOverlaySessionPicker
160+ active.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
161+ active.request.StartupOverlay = protocol.StartupOverlaySessionPicker
161162
162163 for _, tt := range []struct {
163164 name string
@@ -170,11 +171,11 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
170171 ledger := newRouteLedger()
171172 target := routeTestTarget(1)
172173 if tt.matched {
173- stored := routeTestCandidate(1, ports.RouteOriginLocal)
174+ stored := routeTestCandidate(1, protocol.RouteOriginLocal)
174175 stored.originKey = "local"
175176 stored.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
176- stored.request.NavigationCapabilities = ports.NavigationCapabilityHomePicker
177- stored.request.StartupOverlay = ports.StartupOverlaySessionPicker
177+ stored.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
178+ stored.request.StartupOverlay = protocol.StartupOverlaySessionPicker
178179 _, err := ledger.commit(stored)
179180 require.NoError(t, err)
180181 }
@@ -186,7 +187,7 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
186187
187188 require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
188189 require.Zero(t, request.NavigationCapabilities)
189- require.Equal(t, ports.StartupOverlayNone, request.StartupOverlay)
190+ require.Equal(t, protocol.StartupOverlayNone, request.StartupOverlay)
190191 require.NoError(t, validateAttachRequest(request))
191192 })
192193 }
@@ -194,7 +195,7 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
194195
195196 func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
196197 ledger := newRouteLedger()
197- work := routeTestCandidate(1, ports.RouteOriginRemote)
198+ work := routeTestCandidate(1, protocol.RouteOriginRemote)
198199 work.originKey = "remote"
199200 work.request.Remote = true
200201 work.request.OriginKey = "remote"
@@ -206,7 +207,7 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
206207 _, err := ledger.commit(work)
207208 require.NoError(t, err)
208209
209- agents := routeTestCandidate(2, ports.RouteOriginRemote)
210+ agents := routeTestCandidate(2, protocol.RouteOriginRemote)
210211 agents.originKey = "remote"
211212 agents.request.Remote = true
212213 agents.request.OriginKey = "remote"
@@ -220,32 +221,32 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
220221 })
221222
222223 require.True(t, request.Remote)
223- require.Equal(t, ports.RouteOriginRemote, request.Origin)
224+ require.Equal(t, protocol.RouteOriginRemote, request.Origin)
224225 require.Equal(t, "remote", request.OriginKey)
225226 require.Nil(t, request.RemoteTarget)
226227 }
227228
228229 func TestRouteAttentionSubscriptionIncludesOnlyActiveOriginRoutes(t *testing.T) {
229230 ledger := newRouteLedger()
230- first := routeTestCandidate(0, ports.RouteOriginRemote)
231+ first := routeTestCandidate(0, protocol.RouteOriginRemote)
231232 first.originKey = "host-a"
232233 firstIdentity, err := ledger.commit(first)
233234 require.NoError(t, err)
234- other := routeTestCandidate(1, ports.RouteOriginLocal)
235+ other := routeTestCandidate(1, protocol.RouteOriginLocal)
235236 _, err = ledger.commit(other)
236237 require.NoError(t, err)
237- otherRemote := routeTestCandidate(2, ports.RouteOriginRemote)
238+ otherRemote := routeTestCandidate(2, protocol.RouteOriginRemote)
238239 otherRemote.originKey = "host-b"
239240 _, err = ledger.commit(otherRemote)
240241 require.NoError(t, err)
241- active := routeTestCandidate(3, ports.RouteOriginRemote)
242+ active := routeTestCandidate(3, protocol.RouteOriginRemote)
242243 active.originKey = "host-a"
243244 _, err = ledger.commit(active)
244245 require.NoError(t, err)
245246
246247 subscription := ledger.attentionSubscription()
247248
248- require.Equal(t, []ports.RouteAttentionTarget{{
249+ require.Equal(t, []protocol.RouteAttentionTarget{{
249250 Ref: firstIdentity.wire(),
250251 Target: first.target,
251252 }}, subscription.Targets)
@@ -253,16 +254,16 @@ func TestRouteAttentionSubscriptionIncludesOnlyActiveOriginRoutes(t *testing.T)
253254
254255 func TestCommittedIdentityRenamesActiveRouteInPlace(t *testing.T) {
255256 ledger := newRouteLedger()
256- original := routeTestCandidate(0, ports.RouteOriginLocal)
257- original.target = ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "0"}
257+ original := routeTestCandidate(0, protocol.RouteOriginLocal)
258+ original.target = protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "0"}
258259 original.presentation.name = "0"
259260 original.request.SessionName = "0"
260261 identity, err := ledger.commit(original)
261262 require.NoError(t, err)
262263 before := ledger.snapshot()
263264
264- committed, err := ledger.commitCommittedIdentity(ports.CommittedRouteIdentity{
265- Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "vps-infra"},
265+ committed, err := ledger.commitCommittedIdentity(protocol.CommittedRouteIdentity{
266+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "vps-infra"},
266267 Ephemeral: false,
267268 })
268269 require.NoError(t, err)
@@ -280,11 +281,11 @@ func TestCommittedIdentityRenamesActiveRouteInPlace(t *testing.T) {
280281
281282 func TestCommittedIdentityDoesNotReassignHomeRoute(t *testing.T) {
282283 ledger := newRouteLedger()
283- home := routeTestCandidate(1, ports.RouteOriginLocal)
284+ home := routeTestCandidate(1, protocol.RouteOriginLocal)
284285 home.home = true
285286 homeIdentity, err := ledger.commit(home)
286287 require.NoError(t, err)
287- committed, err := ledger.commitCommittedIdentity(ports.CommittedRouteIdentity{
288+ committed, err := ledger.commitCommittedIdentity(protocol.CommittedRouteIdentity{
288289 Target: routeTestTarget(2),
289290 Ephemeral: true,
290291 })
@@ -295,7 +296,7 @@ func TestCommittedIdentityDoesNotReassignHomeRoute(t *testing.T) {
295296 require.True(t, ok)
296297 require.Equal(t, committed, active.identity)
297298
298- _, err = ledger.commit(routeTestCandidate(1, ports.RouteOriginLocal))
299+ _, err = ledger.commit(routeTestCandidate(1, protocol.RouteOriginLocal))
299300 require.NoError(t, err)
300301 require.True(t, mustRouteRecord(t, ledger, ledger.activeRef()).home, "upserting the home route must retain its marker")
301302 }
@@ -309,7 +310,7 @@ func TestRouteLedgerConcurrentInitialAttachmentsKeepOneHome(t *testing.T) {
309310 wg.Add(1)
310311 go func() {
311312 defer wg.Done()
312- _, err := ledger.commitAttach(routeTestCandidate(i, ports.RouteOriginLocal))
313+ _, err := ledger.commitAttach(routeTestCandidate(i, protocol.RouteOriginLocal))
313314 errs <- err
314315 }()
315316 }
@@ -333,22 +334,22 @@ func TestRouteLedgerConcurrentInitialAttachmentsKeepOneHome(t *testing.T) {
333334
334335 func TestRouteLedgerTransitionRejectsConcurrentCommit(t *testing.T) {
335336 ledger := newRouteLedger()
336- selected, err := ledger.commit(routeTestCandidate(1, ports.RouteOriginLocal))
337+ selected, err := ledger.commit(routeTestCandidate(1, protocol.RouteOriginLocal))
337338 require.NoError(t, err)
338- _, err = ledger.commit(routeTestCandidate(2, ports.RouteOriginLocal))
339+ _, err = ledger.commit(routeTestCandidate(2, protocol.RouteOriginLocal))
339340 require.NoError(t, err)
340341 selection, ok := ledger.navigationSelection(selected.wireNavigationAction(routeGeneration(ledger.snapshot().Generation)))
341342 require.True(t, ok)
342343
343- _, err = ledger.commit(routeTestCandidate(3, ports.RouteOriginLocal))
344+ _, err = ledger.commit(routeTestCandidate(3, protocol.RouteOriginLocal))
344345 require.NoError(t, err)
345- candidate := routeTestCandidate(4, ports.RouteOriginLocal)
346+ candidate := routeTestCandidate(4, protocol.RouteOriginLocal)
346347 err = ledger.commitTransition(selected, selection.snapshotGeneration, selection.active, candidate)
347348 require.ErrorIs(t, err, errRouteStaleSelection)
348349 require.Equal(t, routeTestTarget(3), mustRouteRecord(t, ledger, ledger.activeRef()).target)
349350 }
350351
351-func mustRouteRecord(t *testing.T, ledger *routeLedger, ref ports.RouteRef) routeRecord {
352+func mustRouteRecord(t *testing.T, ledger *routeLedger, ref protocol.RouteRef) routeRecord {
352353 t.Helper()
353354 record, ok := ledger.lookup(ref)
354355 require.True(t, ok)
@@ -357,7 +358,7 @@ func mustRouteRecord(t *testing.T, ledger *routeLedger, ref ports.RouteRef) rout
357358
358359 func TestRouteLedgerUpsertChangesGenerationWithoutChangingOpaqueKey(t *testing.T) {
359360 ledger := newRouteLedger()
360- candidate := routeTestCandidate(1, ports.RouteOriginRemote)
361+ candidate := routeTestCandidate(1, protocol.RouteOriginRemote)
361362 first, err := ledger.commit(candidate)
362363 require.NoError(t, err)
363364
@@ -387,7 +388,7 @@ type routeTestConnector struct {
387388 attachCalls int
388389 restoreCalls int
389390 restoreErr error
390- restoreTarget ports.ExactSessionTarget
391+ restoreTarget protocol.ExactSessionTarget
391392 }
392393
393394 func (c *routeTestConnector) Resume(context.Context, routeRecord) (routeTransitionCommit, error) {
@@ -410,11 +411,11 @@ func (c *routeTestConnector) Attach(_ context.Context, record routeRecord) (rout
410411 return routeTransitionCommit{}, c.attachErr
411412 }
412413 return routeTransitionCommit{
413- identity: ports.CommittedRouteIdentity{Target: record.target},
414+ identity: protocol.CommittedRouteIdentity{Target: record.target},
414415 presentation: routePresentation{
415416 name: record.presentation.name,
416417 kind: record.presentation.kind,
417- reachability: ports.RouteReachabilityReachable,
418+ reachability: protocol.RouteReachabilityReachable,
418419 },
419420 resumeToken: 123,
420421 }, nil
@@ -422,12 +423,12 @@ func (c *routeTestConnector) Attach(_ context.Context, record routeRecord) (rout
422423
423424 func TestRouteLedgerNavigateResumesThenFallsBackAndRestoresOrigin(t *testing.T) {
424425 ledger := newRouteLedger()
425- candidate := routeTestCandidate(2, ports.RouteOriginDiscovery)
426+ candidate := routeTestCandidate(2, protocol.RouteOriginDiscovery)
426427 candidate.originKey = "daemon-discovered"
427428 candidate.resumeToken = 77
428429 identity, err := ledger.commit(candidate)
429430 require.NoError(t, err)
430- _, err = ledger.commit(routeTestCandidate(4, ports.RouteOriginLocal))
431+ _, err = ledger.commit(routeTestCandidate(4, protocol.RouteOriginLocal))
431432 require.NoError(t, err)
432433
433434 connector := &routeTestConnector{resumeErr: errRouteResumeUnavailable}
@@ -440,18 +441,18 @@ func TestRouteLedgerNavigateResumesThenFallsBackAndRestoresOrigin(t *testing.T)
440441
441442 active, ok := ledger.lookup(ledger.activeRef())
442443 require.True(t, ok)
443- require.Equal(t, ports.RouteOriginDiscovery, active.origin)
444+ require.Equal(t, protocol.RouteOriginDiscovery, active.origin)
444445 require.Equal(t, "daemon-discovered", active.originKey)
445- require.Equal(t, ports.RouteOriginDiscovery, active.request.Origin)
446+ require.Equal(t, protocol.RouteOriginDiscovery, active.request.Origin)
446447 require.Equal(t, candidate.target, active.target)
447448 require.Greater(t, active.identity.generation, identity.generation)
448449 }
449450
450451 func TestRouteLedgerNavigateIsTransactionalOnFailureOrTargetChange(t *testing.T) {
451452 ledger := newRouteLedger()
452- identity, err := ledger.commit(routeTestCandidate(3, ports.RouteOriginLocal))
453+ identity, err := ledger.commit(routeTestCandidate(3, protocol.RouteOriginLocal))
453454 require.NoError(t, err)
454- _, err = ledger.commit(routeTestCandidate(5, ports.RouteOriginLocal))
455+ _, err = ledger.commit(routeTestCandidate(5, protocol.RouteOriginLocal))
455456 require.NoError(t, err)
456457 before := ledger.snapshot()
457458 action := identity.wireNavigationAction(routeGeneration(before.Generation))
@@ -475,7 +476,7 @@ func TestRouteLedgerNavigateIsTransactionalOnFailureOrTargetChange(t *testing.T)
475476
476477 func TestRouteLedgerNavigationRejectsStaleActionsAndActiveNoOp(t *testing.T) {
477478 ledger := newRouteLedger()
478- active, err := ledger.commit(routeTestCandidate(6, ports.RouteOriginLocal))
479+ active, err := ledger.commit(routeTestCandidate(6, protocol.RouteOriginLocal))
479480 require.NoError(t, err)
480481 firstSnapshot := ledger.snapshot()
481482 before := ledger.snapshot()
@@ -483,7 +484,7 @@ func TestRouteLedgerNavigationRejectsStaleActionsAndActiveNoOp(t *testing.T) {
483484 require.NoError(t, ledger.navigate(context.Background(), active.wireNavigationAction(routeGeneration(firstSnapshot.Generation)), nil))
484485 require.Equal(t, before, ledger.snapshot())
485486
486- _, err = ledger.commit(routeTestCandidate(7, ports.RouteOriginLocal))
487+ _, err = ledger.commit(routeTestCandidate(7, protocol.RouteOriginLocal))
487488 require.NoError(t, err)
488489 stale := active.wireNavigationAction(routeGeneration(firstSnapshot.Generation))
489490 connector := &routeTestConnector{}
@@ -496,13 +497,13 @@ type mismatchingRouteConnector struct{ *routeTestConnector }
496497
497498 func (c mismatchingRouteConnector) Attach(_ context.Context, record routeRecord) (routeTransitionCommit, error) {
498499 return routeTransitionCommit{
499- identity: ports.CommittedRouteIdentity{Target: routeTestTarget(99)},
500+ identity: protocol.CommittedRouteIdentity{Target: routeTestTarget(99)},
500501 presentation: routePresentation{name: record.presentation.name, kind: record.presentation.kind},
501502 }, nil
502503 }
503504
504-func (i routeIdentity) wireNavigationAction(snapshotGeneration routeGeneration) ports.RouteNavigationAction {
505- return ports.RouteNavigationAction{SnapshotGeneration: uint64(snapshotGeneration), Key: uint64(i.key), Generation: uint64(i.generation)}
505+func (i routeIdentity) wireNavigationAction(snapshotGeneration routeGeneration) protocol.RouteNavigationAction {
506+ return protocol.RouteNavigationAction{SnapshotGeneration: uint64(snapshotGeneration), Key: uint64(i.key), Generation: uint64(i.generation)}
506507 }
507508
508509 func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
@@ -513,7 +514,7 @@ func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
513514 go func() {
514515 defer wg.Done()
515516 for i := byte(0); i < 20; i++ {
516- _, _ = ledger.commit(routeTestCandidate(worker*20+i, ports.RouteOriginLocal))
517+ _, _ = ledger.commit(routeTestCandidate(worker*20+i, protocol.RouteOriginLocal))
517518 _ = ledger.snapshot()
518519 }
519520 }()
@@ -524,7 +525,7 @@ func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
524525
525526 func TestRouteLedgerSeparatesRemoteDaemonOrigins(t *testing.T) {
526527 ledger := newRouteLedger()
527- firstCandidate := routeTestCandidate(40, ports.RouteOriginRemote)
528+ firstCandidate := routeTestCandidate(40, protocol.RouteOriginRemote)
528529 firstCandidate.originKey = "daemon-a"
529530 secondCandidate := firstCandidate
530531 secondCandidate.originKey = "daemon-b"
M internal/usecase/client/same_peer_switch.go +2 -2
@@ -4,7 +4,7 @@ import (
44 "context"
55 "sync"
66
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 )
99
1010 // samePeerInputGate holds raw input behind a requested in-band switch. It does
@@ -20,7 +20,7 @@ type samePeerInputGate struct {
2020
2121 type samePeerSwitchPending struct {
2222 requestID uint64
23- target ports.ExactSessionTarget
23+ target protocol.ExactSessionTarget
2424 }
2525
2626 func newSamePeerInputGate() *samePeerInputGate {
M internal/usecase/client/same_peer_switch_test.go +2 -1
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
@@ -33,7 +34,7 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
3334 resultCh := make(chan attachResult, 1)
3435 go func() { resultCh <- attempt.run(ctx) }()
3536
36- exact := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}
37+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}
3738 target := ports.AttachTarget{
3839 Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &exact,
3940 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
M internal/usecase/client/transport_observability_test.go +3 -2
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/client"
1819 )
1920
@@ -50,7 +51,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
5051 transport.EXPECT().Send(isType(ports.MsgAck)).Run(func(ports.Frame) { close(acked) }).Return(nil).Once()
5152 unblock := scriptRecv(transport,
5253 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
53- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("flush-before-observe"), New: 3}))},
54+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("flush-before-observe"), New: 3}))},
5455 )
5556 defer unblock()
5657 transport.EXPECT().Close().Return(nil).Once()
@@ -90,7 +91,7 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
9091 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
9192 unblock := scriptRecv(transport,
9293 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
93- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},
94+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},
9495 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
9596 )
9697 defer unblock()
M internal/usecase/client/wire_external_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
11 package client_test
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
47
5-func mustMarshalOutput(m ports.Output) []byte {
8+func mustMarshalOutput(m protocol.Output) []byte {
69 payload, err := ports.MarshalOutput(m)
710 if err != nil {
811 panic(err)
M internal/usecase/client/wire_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
11 package client
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
47
5-func mustMarshalOutput(m ports.Output) []byte {
8+func mustMarshalOutput(m protocol.Output) []byte {
69 payload, err := ports.MarshalOutput(m)
710 if err != nil {
811 panic(err)
M internal/usecase/daemon/attachment_effect_gate_test.go +14 -13
@@ -12,6 +12,7 @@ import (
1212 renderer "github.com/bnema/vev-vt"
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 "github.com/bnema/vev/internal/usecase/keys"
1617 )
1718
@@ -611,7 +612,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
611612 },
612613 {
613614 name: "theme",
614- frame: ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{
615+ frame: ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{
615616 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
616617 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
617618 })},
@@ -624,7 +625,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
624625 setup: func(_ *Daemon, _ *session, ac *attachedClient, _ *transactionalResizePTY) {
625626 ac.output.next = 3
626627 },
627- frame: ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: 1, State: 1})},
628+ frame: ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: 1, State: 1})},
628629 assert: func(t *testing.T, ac *attachedClient, _ *transactionalResizePTY) {
629630 ac.sendMu.Lock()
630631 defer ac.sendMu.Unlock()
@@ -687,14 +688,14 @@ func TestAttachedRouteSnapshotIsAcceptedAsAnAttachmentValue(t *testing.T) {
687688 token.lease = rc.attachmentLease(ac)
688689 ac.installTestAttachmentCapability(token)
689690
690- activeRef := ports.RouteRef{Key: 8, Generation: 4}
691+ activeRef := protocol.RouteRef{Key: 8, Generation: 4}
691692 activeTarget := testRouteTarget(source.name, 8)
692- snapshot := ports.RecentRouteSnapshot{
693+ snapshot := protocol.RecentRouteSnapshot{
693694 Generation: 4,
694695 Active: activeRef,
695- ActiveEntry: ports.RecentRouteEntry{Key: 8, Generation: 4, Target: activeTarget, Name: source.name, Kind: ports.RouteKindLocal},
696+ ActiveEntry: protocol.RecentRouteEntry{Key: 8, Generation: 4, Target: activeTarget, Name: source.name, Kind: protocol.RouteKindLocal},
696697 Home: activeRef,
697- Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)},
698+ Entries: []protocol.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, protocol.RouteKindLocal)},
698699 }
699700 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)
700701 require.NoError(t, err)
@@ -711,12 +712,12 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
711712 d.mu.Lock()
712713 d.sessions[target.id] = target
713714 d.mu.Unlock()
714- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
715+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
715716 Generation: 2,
716- Active: ports.RouteRef{Key: 2, Generation: 2},
717- ActiveEntry: testRouteEntry(2, 2, source.name, 2, ports.RouteKindLocal),
718- Previous: ports.RouteRef{Key: 1, Generation: 1},
719- Entries: []ports.RecentRouteEntry{testRouteEntry(1, 1, "target", 1, ports.RouteKindLocal)},
717+ Active: protocol.RouteRef{Key: 2, Generation: 2},
718+ ActiveEntry: testRouteEntry(2, 2, source.name, 2, protocol.RouteKindLocal),
719+ Previous: protocol.RouteRef{Key: 1, Generation: 1},
720+ Entries: []protocol.RecentRouteEntry{testRouteEntry(1, 1, "target", 1, protocol.RouteKindLocal)},
720721 })
721722
722723 transport := &closeTrackingTransport{}
@@ -744,7 +745,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
744745 break
745746 }
746747 require.True(t, result.OK, result.Text)
747- var action ports.RouteNavigationAction
748+ var action protocol.RouteNavigationAction
748749 for _, frame := range frames {
749750 if frame.Type != ports.MsgNavigateRecentRoute {
750751 continue
@@ -752,7 +753,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
752753 action, err = ports.UnmarshalRouteNavigationAction(frame.Payload)
753754 require.NoError(t, err)
754755 }
755- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}, action)
756+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}, action)
756757 require.False(t, transport.Closed(), "client navigation must not tear down the attachment")
757758 }
758759
M internal/usecase/daemon/attachment_output.go +5 -4
@@ -9,6 +9,7 @@ import (
99 renderer "github.com/bnema/vev-vt/ansi"
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 )
1314
1415 // attachmentOutput owns the terminal-output dependency chain and emitted
@@ -32,7 +33,7 @@ type attachmentOutput struct {
3233 forceSnapshot bool
3334 initialized bool
3435 lastCursor cursorOut
35- lastRoutePosition ports.RoutePosition
36+ lastRoutePosition protocol.RoutePosition
3637 graphicsOutput *graphicsOutputState
3738 graphicsUnsupportedWarned atomic.Bool
3839 attachment *attachedClient
@@ -139,7 +140,7 @@ func (o *attachmentOutput) prepareFrame(d *Daemon, state *capturedRenderState, f
139140 }
140141 cursor := o.prepareCursorTail(desired, len(ansi.data) > 0)
141142 graphicsReset := reset || o.forceSnapshot || !o.initialized
142- graphicsBudget := max(ports.MaxOutputDataLen-len(ansi.data)-len(cursor.data), 0)
143+ graphicsBudget := max(protocol.MaxOutputDataLen-len(ansi.data)-len(cursor.data), 0)
143144 preparedGraphics, err := graphicsOutputDataWithDaemonLimit(d, state, o.attachment, graphicsReset, graphicsBudget)
144145 if err != nil {
145146 return nil, err
@@ -379,7 +380,7 @@ func (p *preparedOutput) currentLocked() bool {
379380 }
380381
381382 func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (ports.Frame, error) {
382- payload, err := ports.MarshalOutput(ports.Output{
383+ payload, err := ports.MarshalOutput(protocol.Output{
383384 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,
384385 Size: size, Full: full, Data: data,
385386 })
@@ -440,7 +441,7 @@ func (s *attachmentOutput) rebaseAttachment() {
440441 return
441442 }
442443 s.rebase()
443- s.lastRoutePosition = ports.RoutePosition{}
444+ s.lastRoutePosition = protocol.RoutePosition{}
444445 }
445446
446447 // rebaseLocked is called while the attachment view lock is held. Publishing
M internal/usecase/daemon/attachment_output_test.go +3 -2
@@ -10,6 +10,7 @@ import (
1010 "github.com/bnema/vev-vt/graphics"
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 "github.com/bnema/vev/internal/usecase/layout"
1415 "github.com/stretchr/testify/require"
1516 )
@@ -468,7 +469,7 @@ func TestAttachmentOutputRebaseRetiresAttachmentState(t *testing.T) {
468469 output.next = 3
469470 output.acked = 2
470471 output.lastCursor = cursorOut{valid: true, row: 2, col: 3}
471- output.lastRoutePosition = ports.RoutePosition{ActiveTabID: "tab"}
472+ output.lastRoutePosition = protocol.RoutePosition{ActiveTabID: "tab"}
472473
473474 output.rebaseAttachment()
474475
@@ -477,7 +478,7 @@ func TestAttachmentOutputRebaseRetiresAttachmentState(t *testing.T) {
477478 require.Zero(t, output.acked)
478479 require.True(t, output.forceSnapshot)
479480 require.Equal(t, cursorOut{valid: true, row: 2, col: 3}, output.lastCursor, "the forced frame will reassert cursor state")
480- require.Equal(t, ports.RoutePosition{}, output.lastRoutePosition)
481+ require.Equal(t, protocol.RoutePosition{}, output.lastRoutePosition)
481482 }
482483
483484 func TestAttachmentOutputDefaultsAndNormalizesWindow(t *testing.T) {
M internal/usecase/daemon/attachment_parking_test.go +2 -1
@@ -7,6 +7,7 @@ import (
77 "github.com/stretchr/testify/require"
88
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 type resumeWelcomeFailureTransport struct {
@@ -137,7 +138,7 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
137138 sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
138139 require.NoError(t, err)
139140 oldToken := ac.resumeToken
140- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
141+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
141142 d.clientGone(sess, ac, oldTransport, false)
142143
143144 resumedTransport := &closeTrackingTransport{}
M internal/usecase/daemon/attachment_render_test.go +2 -1
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 "github.com/stretchr/testify/require"
1314 )
1415
@@ -285,7 +286,7 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
285286 token := sess.captureAttachmentCapability(ac, tr)
286287 d.handleAttachmentClientFrame(token, ports.Frame{
287288 Type: ports.MsgResize,
288- Payload: mustMarshalResize(ports.Resize{Size: size}),
289+ Payload: mustMarshalResize(protocol.Resize{Size: size}),
289290 })
290291 }
291292 resize(second, secondTransport, domain.Size{Cols: 120, Rows: 50})
M internal/usecase/daemon/attachment_route_identity_test.go +4 -3
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/stretchr/testify/require"
1011 )
1112
@@ -28,7 +29,7 @@ func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) {
2829 token := source.captureAttachmentCapability(ac, transport)
2930 token.lease = rc.attachmentLease(ac)
3031 ac.installTestAttachmentCapability(token)
31- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
32+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
3233
3334 target := &session{
3435 sessionCore: sessionCore{
@@ -54,7 +55,7 @@ func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) {
5455 require.NoError(t, err)
5556 d.deferAttachmentTransitionCleanups(transition)
5657
57- var identity ports.CommittedRouteIdentity
58+ var identity protocol.CommittedRouteIdentity
5859 found := false
5960 for _, frame := range transport.Sends() {
6061 if frame.Type != ports.MsgCommittedRouteIdentity {
@@ -78,7 +79,7 @@ func TestAttachmentTransitionTearsDownAfterCommittedIdentitySendFailure(t *testi
7879 token := source.captureAttachmentCapability(ac, transport)
7980 token.lease = rc.attachmentLease(ac)
8081 ac.installTestAttachmentCapability(token)
81- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
82+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
8283
8384 target := &session{
8485 sessionCore: sessionCore{
M internal/usecase/daemon/client.go +19 -18
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
1617 "github.com/bnema/vev/internal/usecase/keys"
1718 "github.com/bnema/vev/internal/usecase/mouse"
1819 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -40,8 +41,8 @@ type attachedClient struct {
4041 overlayOnce sync.Once
4142 clientID [16]byte
4243 terminalCapabilities ports.TerminalCapabilities
43- navigationCapabilities ports.NavigationCapabilities
44- startupOverlay ports.StartupOverlay
44+ navigationCapabilities protocol.NavigationCapabilities
45+ startupOverlay protocol.StartupOverlay
4546 // lifecycle is the sole authority for attachment capability publication,
4647 // effect admission, transition freeze/drain, and connection generation.
4748 lifecycle attachmentLifecycle
@@ -86,15 +87,15 @@ type attachedClient struct {
8687 linkMu sync.Mutex
8788 sendMu sync.Mutex
8889 routeMu sync.RWMutex
89- routeSnapshot ports.RecentRouteSnapshot
90+ routeSnapshot protocol.RecentRouteSnapshot
9091 pendingRouteIdentity bool
9192 samePeerOfferMu sync.Mutex
92- samePeerOffer *ports.ExactSessionTarget
93+ samePeerOffer *protocol.ExactSessionTarget
9394 parkedRouteMu sync.Mutex
9495 parkedRoute *parkedRouteLease
9596 parkedRouteOutput atomic.Bool
9697 parkedRouteFullPending atomic.Bool
97- routeAttentionSubscription ports.RouteAttentionSubscription
98+ routeAttentionSubscription protocol.RouteAttentionSubscription
9899 // routeCreatedSession marks a session created by this attachment's route.
99100 // A handshake that never commits Welcome must tear down that exact empty
100101 // session, while an attachment routed to an existing session must not.
@@ -336,8 +337,8 @@ func (ac *attachedClient) currentTransportIs(tr ports.Transport) bool {
336337
337338 // setRouteSnapshot records a client-owned route view and reports whether a
338339 // rename identity was deferred until that first usable publication.
339-func (ac *attachedClient) setRouteSnapshot(snapshot ports.RecentRouteSnapshot) bool {
340- snapshot.Entries = append([]ports.RecentRouteEntry(nil), snapshot.Entries...)
340+func (ac *attachedClient) setRouteSnapshot(snapshot protocol.RecentRouteSnapshot) bool {
341+ snapshot.Entries = append([]protocol.RecentRouteEntry(nil), snapshot.Entries...)
341342 ac.routeMu.Lock()
342343 ac.routeSnapshot = snapshot
343344 replayPendingIdentity := snapshot.Generation != 0 && ac.pendingRouteIdentity
@@ -361,22 +362,22 @@ func (ac *attachedClient) deferRouteIdentityUntilSnapshot() bool {
361362 return true
362363 }
363364
364-func (ac *attachedClient) routeSnapshotCopy() ports.RecentRouteSnapshot {
365+func (ac *attachedClient) routeSnapshotCopy() protocol.RecentRouteSnapshot {
365366 ac.routeMu.RLock()
366367 defer ac.routeMu.RUnlock()
367368 snapshot := ac.routeSnapshot
368- snapshot.Entries = append([]ports.RecentRouteEntry(nil), snapshot.Entries...)
369+ snapshot.Entries = append([]protocol.RecentRouteEntry(nil), snapshot.Entries...)
369370 return snapshot
370371 }
371372
372-func (ac *attachedClient) setRouteAttentionSubscription(subscription ports.RouteAttentionSubscription) {
373- subscription.Targets = append([]ports.RouteAttentionTarget(nil), subscription.Targets...)
373+func (ac *attachedClient) setRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) {
374+ subscription.Targets = append([]protocol.RouteAttentionTarget(nil), subscription.Targets...)
374375 ac.routeMu.Lock()
375376 ac.routeAttentionSubscription = subscription
376377 ac.routeMu.Unlock()
377378 }
378379
379-func (ac *attachedClient) routeAttentionTarget(ref ports.RouteRef) (ports.ExactSessionTarget, bool) {
380+func (ac *attachedClient) routeAttentionTarget(ref protocol.RouteRef) (protocol.ExactSessionTarget, bool) {
380381 ac.routeMu.RLock()
381382 defer ac.routeMu.RUnlock()
382383 for _, target := range ac.routeAttentionSubscription.Targets {
@@ -384,7 +385,7 @@ func (ac *attachedClient) routeAttentionTarget(ref ports.RouteRef) (ports.ExactS
384385 return target.Target, true
385386 }
386387 }
387- return ports.ExactSessionTarget{}, false
388+ return protocol.ExactSessionTarget{}, false
388389 }
389390
390391 func (ac *attachedClient) ackOutputState(epoch, state uint64) {
@@ -613,8 +614,8 @@ type attachClientOptions struct {
613614 maxOutputInFlight uint8
614615 terminalCapabilities ports.TerminalCapabilities
615616 capabilitiesSet bool
616- navigationCapabilities ports.NavigationCapabilities
617- startupOverlay ports.StartupOverlay
617+ navigationCapabilities protocol.NavigationCapabilities
618+ startupOverlay protocol.StartupOverlay
618619 }
619620
620621 func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {
@@ -882,7 +883,7 @@ func (d *Daemon) firstPaintWithLease(sess *session, ac *attachedClient, lease *a
882883
883884 // detachIfCurrent clears the client iff ac is the current one, reporting
884885
885-func themeFromMessage(msg ports.Theme) themeui.Theme {
886+func themeFromMessage(msg protocol.Theme) themeui.Theme {
886887 return themeui.Theme{
887888 Foreground: msg.Foreground,
888889 Background: msg.Background,
@@ -897,7 +898,7 @@ func themeFromMessage(msg ports.Theme) themeui.Theme {
897898 }
898899 }
899900
900-func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme) {
901+func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg protocol.Theme) {
901902 clientTheme := themeFromMessage(msg)
902903 ac.setClientTheme(clientTheme)
903904
@@ -907,7 +908,7 @@ func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme)
907908 d.invalidateRender(sess, ac, true, "client.go")
908909 }
909910
910-func (d *Daemon) applyThemeForAttachment(effect *attachmentEffect, msg ports.Theme) {
911+func (d *Daemon) applyThemeForAttachment(effect *attachmentEffect, msg protocol.Theme) {
911912 if !effect.current() {
912913 return
913914 }
M internal/usecase/daemon/client_frame_routing.go +6 -5
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/bnema/vev/internal/usecase/command"
1011 )
1112
@@ -169,15 +170,15 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
169170 }
170171 message := "route navigation failed"
171172 switch failure.Code {
172- case ports.RouteFailureStaleSelection:
173+ case protocol.RouteFailureStaleSelection:
173174 message = "that recent route is no longer available"
174- case ports.RouteFailureTargetChanged:
175+ case protocol.RouteFailureTargetChanged:
175176 message = "that recent route changed before attach"
176- case ports.RouteFailureOriginUnavailable:
177+ case protocol.RouteFailureOriginUnavailable:
177178 message = "the original route is no longer available"
178- case ports.RouteFailureUnavailable:
179+ case protocol.RouteFailureUnavailable:
179180 message = "the selected route is unavailable"
180- case ports.RouteFailureNoSuchRoute:
181+ case protocol.RouteFailureNoSuchRoute:
181182 message = "that recent route no longer exists"
182183 }
183184 d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil)
M internal/usecase/daemon/client_transport_errors.go +3 -1
@@ -1,6 +1,8 @@
11 package daemon
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+)
46
57 // clientGone detaches ac if it is still the session's current client. The
68 // session remains registered and headless after the client is gone.
M internal/usecase/daemon/clipboard.go +7 -6
@@ -8,6 +8,7 @@ import (
88 "os"
99
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 scopy "github.com/bnema/vev/internal/usecase/copy"
1213 )
1314
@@ -26,14 +27,14 @@ var (
2627 clipPasteCloseMarker = []byte("\x1b[201~")
2728 )
2829
29-func (d *Daemon) handleSequencedImagePushForAttachment(effect *attachmentEffect, _ uint64, ip ports.ImagePush) {
30+func (d *Daemon) handleSequencedImagePushForAttachment(effect *attachmentEffect, _ uint64, ip protocol.ImagePush) {
3031 if !effect.current() {
3132 return
3233 }
3334 d.handleImagePushForAttachment(effect, ip)
3435 }
3536
36-func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip ports.ImagePush) {
37+func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip protocol.ImagePush) {
3738 if len(ip.Data) == 0 || len(ip.Data) > maxImagePushSize || !effect.current() {
3839 return
3940 }
@@ -55,7 +56,7 @@ func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip ports
5556 // failure is logged and the push is dropped: the client has already
5657 // consumed the Ctrl+V keystroke that triggered it, so there is nothing left
5758 // to re-trigger from here.
58-func (d *Daemon) handleImagePush(sess *session, ip ports.ImagePush) {
59+func (d *Daemon) handleImagePush(sess *session, ip protocol.ImagePush) {
5960 if len(ip.Data) == 0 {
6061 return
6162 }
@@ -73,7 +74,7 @@ func (d *Daemon) handleImagePush(sess *session, ip ports.ImagePush) {
7374
7475 // writeClipboardImage writes ip's bytes to a new 0600 temp file and records
7576 // the path on sess for best-effort cleanup at session end (killSession).
76-func (d *Daemon) writeClipboardImage(sess *session, ip ports.ImagePush) (string, error) {
77+func (d *Daemon) writeClipboardImage(sess *session, ip protocol.ImagePush) (string, error) {
7778 path, err := d.createClipboardImage(ip)
7879 if err != nil {
7980 return "", err
@@ -84,7 +85,7 @@ func (d *Daemon) writeClipboardImage(sess *session, ip ports.ImagePush) (string,
8485 return path, nil
8586 }
8687
87-func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip ports.ImagePush) (string, error) {
88+func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip protocol.ImagePush) (string, error) {
8889 path, err := d.createClipboardImage(ip)
8990 if err != nil {
9091 return "", err
@@ -105,7 +106,7 @@ func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip p
105106 return path, nil
106107 }
107108
108-func (d *Daemon) createClipboardImage(ip ports.ImagePush) (string, error) {
109+func (d *Daemon) createClipboardImage(ip protocol.ImagePush) (string, error) {
109110 dir := d.tempDir
110111 if dir == "" {
111112 dir = os.TempDir()
M internal/usecase/daemon/clipboard_test.go +7 -6
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
2020 portsmocks "github.com/bnema/vev/internal/ports/mocks"
21+ "github.com/bnema/vev/internal/protocol"
2122 scopy "github.com/bnema/vev/internal/usecase/copy"
2223 )
2324
@@ -47,7 +48,7 @@ func TestHandleImagePushWritesFileWithExactBytesAndMode0600(t *testing.T) {
4748 d.tempDir = t.TempDir()
4849
4950 data := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0x01, 0x02, 0x03}
50- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: data})
51+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: data})
5152
5253 injected := <-writes
5354 path := string(injected)
@@ -73,7 +74,7 @@ func TestHandleImagePushInjectsPathWithoutBracketedPasteWrapping(t *testing.T) {
7374 pane := sess.tabs[0].focusedPane()
7475 require.False(t, pane.screen.BracketedPasteMode())
7576
76- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})
77+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
7778
7879 injected := <-writes
7980 require.False(t, hasBracketedPasteMarkers(injected), "path must not be wrapped when the pane is not in bracketed-paste mode")
@@ -90,7 +91,7 @@ func TestHandleImagePushInjectsPathWithBracketedPasteWrapping(t *testing.T) {
9091 pane.screen.Write([]byte("\x1b[?2004h"))
9192 require.True(t, pane.screen.BracketedPasteMode())
9293
93- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})
94+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
9495
9596 injected := <-writes
9697 require.True(t, len(injected) > len(clipPasteOpenMarker)+len(clipPasteCloseMarker))
@@ -114,7 +115,7 @@ func TestHandleImagePushRejectsOversizedPayloadWithoutWritingOrInjecting(t *test
114115 d.tempDir = dir
115116
116117 huge := make([]byte, maxImagePushSize+1)
117- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: huge})
118+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: huge})
118119
119120 select {
120121 case w := <-writes:
@@ -134,7 +135,7 @@ func TestHandleImagePushIgnoresEmptyPayload(t *testing.T) {
134135 dir := t.TempDir()
135136 d.tempDir = dir
136137
137- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: nil})
138+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: nil})
138139
139140 select {
140141 case w := <-writes:
@@ -170,7 +171,7 @@ func TestKillSessionRemovesClipboardTempFiles(t *testing.T) {
170171 // killSession requires the session to be registered in the daemon's
171172 // registry (newManualSessionWithPTYs already does this via d.sessions).
172173
173- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})
174+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
174175 injected := <-writes
175176 path := string(injected)
176177 require.FileExists(t, path)
M internal/usecase/daemon/coderabbit_regression_test.go +6 -6
@@ -350,9 +350,9 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
350350 d.mu.Unlock()
351351 close(catalogue.release)
352352
353- var protocol *protoErr
354- require.ErrorAs(t, <-result, &protocol)
355- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
353+ var protocolErr *protoErr
354+ require.ErrorAs(t, <-result, &protocolErr)
355+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
356356 require.Empty(t, catalogue.MetadataUpdates())
357357 d.mu.Lock()
358358 defer d.mu.Unlock()
@@ -443,9 +443,9 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
443443 }
444444 close(catalogue.release)
445445
446- var protocol *protoErr
447- require.ErrorAs(t, <-result, &protocol)
448- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
446+ var protocolErr *protoErr
447+ require.ErrorAs(t, <-result, &protocolErr)
448+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
449449 require.Empty(t, catalogue.MetadataUpdates())
450450 d.mu.Lock()
451451 defer d.mu.Unlock()
M internal/usecase/daemon/config_test.go +2 -1
@@ -9,6 +9,7 @@ import (
99 renderer "github.com/bnema/vev-vt"
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 "github.com/bnema/vev/internal/usecase/keys"
1314 themeui "github.com/bnema/vev/internal/usecase/theme"
1415 )
@@ -63,7 +64,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
6364 clientPalette := [16]renderer.RGB{}
6465 clientPalette[1] = renderer.RGB{R: 1, G: 2, B: 3}
6566 clientPalette[12] = renderer.RGB{R: 4, G: 5, B: 6}
66- d.applyTheme(sess, ac, ports.Theme{
67+ d.applyTheme(sess, ac, protocol.Theme{
6768 HasForeground: true,
6869 Foreground: renderer.RGB{R: 7, G: 8, B: 9},
6970 HasBackground: true,
M internal/usecase/daemon/daemon.go +4 -3
@@ -17,6 +17,7 @@ import (
1717
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
20+ "github.com/bnema/vev/internal/protocol"
2021 "github.com/bnema/vev/internal/usecase/keys"
2122 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
2223 )
@@ -1286,7 +1287,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
12861287 failAttachment()
12871288 return
12881289 }
1289- if ac.startupOverlay == ports.StartupOverlaySessionPicker {
1290+ if ac.startupOverlay == protocol.StartupOverlaySessionPicker {
12901291 d.enterPicker(sess, ac)
12911292 }
12921293 postWelcomeTicket.End()
@@ -1449,7 +1450,7 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
14491450 // route resolves a Hello to a session and a freshly attached client, creating
14501451 // the session for ephemeral/new intents. Direct package callers retain the
14511452 // daemon lifetime context; inbound handshakes use routeWithContext.
1452-func (d *Daemon) validateExactSessionTargetLocked(target ports.ExactSessionTarget) error {
1453+func (d *Daemon) validateExactSessionTargetLocked(target protocol.ExactSessionTarget) error {
14531454 if sess := d.findByNameLocked(target.SessionName); sess != nil {
14541455 if sess.incarnation != target.LifecycleID {
14551456 return &protoErr{ports.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}
@@ -1463,7 +1464,7 @@ func (d *Daemon) validateExactSessionTargetLocked(target ports.ExactSessionTarge
14631464 return nil
14641465 }
14651466
1466-func (d *Daemon) validateExactSessionTarget(ctx context.Context, target ports.ExactSessionTarget) error {
1467+func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol.ExactSessionTarget) error {
14671468 if err := target.Validate(); err != nil {
14681469 return &protoErr{ports.ErrNoSuchSession, "invalid exact session target"}
14691470 }
M internal/usecase/daemon/exact_route_test.go +14 -13
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/stretchr/testify/require"
1011 )
1112
@@ -15,7 +16,7 @@ func TestRouteExactSessionTargetSelectsLifecycle(t *testing.T) {
1516 sess.ephemeral = false
1617 sess.incarnation = remoteLifecycleForTest()
1718 tr, _ := newCapturingTransport(t)
18- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
19+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
1920 hello := ports.Hello{
2021 Version: ports.ProtocolVersion,
2122 Intent: ports.IntentAttach,
@@ -42,13 +43,13 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
4243 Intent: ports.IntentAttach,
4344 Name: "other",
4445 Size: domain.Size{Cols: 80, Rows: 24},
45- ExactTarget: &ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
46+ ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
4647 }
4748
4849 _, _, err := d.routeWithContext(context.Background(), hello, tr)
49- var protocol *protoErr
50- require.ErrorAs(t, err, &protocol)
51- require.Equal(t, ports.ErrNoSuchSession, protocol.code)
50+ var protocolErr *protoErr
51+ require.ErrorAs(t, err, &protocolErr)
52+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
5253 }
5354
5455 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
@@ -56,7 +57,7 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
5657 sess := addControlSession(d, "work", "tab-1", "pane-1")
5758 sess.ephemeral = false
5859 sess.incarnation = remoteLifecycleForTest()
59- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
60+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
6061 replacement := sess.incarnation
6162 replacement[0]++
6263 sess.incarnation = replacement
@@ -65,9 +66,9 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
6566 err := d.validateExactSessionTargetLocked(target)
6667 d.mu.Unlock()
6768
68- var protocol *protoErr
69- require.ErrorAs(t, err, &protocol)
70- require.Equal(t, ports.ErrNoSuchSession, protocol.code)
69+ var protocolErr *protoErr
70+ require.ErrorAs(t, err, &protocolErr)
71+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
7172 }
7273
7374 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
@@ -83,11 +84,11 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
8384 Intent: ports.IntentAttach,
8485 Name: sess.name,
8586 Size: domain.Size{Cols: 80, Rows: 24},
86- ExactTarget: &ports.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
87+ ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
8788 }
8889
8990 _, _, err := d.routeWithContext(context.Background(), hello, tr)
90- var protocol *protoErr
91- require.ErrorAs(t, err, &protocol)
92- require.Equal(t, ports.ErrNoSuchSession, protocol.code)
91+ var protocolErr *protoErr
92+ require.ErrorAs(t, err, &protocolErr)
93+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
9394 }
M internal/usecase/daemon/graphics_output.go +3 -2
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev-vt/graphics"
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
1920 "github.com/bnema/vev/internal/usecase/layout"
2021 themeui "github.com/bnema/vev/internal/usecase/theme"
2122 )
@@ -25,8 +26,8 @@ import (
2526 // attachment-local state is also the ownership boundary for Kitty IDs: raw
2627 // scene IDs are never sent to the outer terminal.
2728 const (
28- maxGraphicsRecordBytes = ports.MaxOutputDataLen
29- maxGraphicsOutputBytes = ports.MaxOutputDataLen
29+ maxGraphicsRecordBytes = protocol.MaxOutputDataLen
30+ maxGraphicsOutputBytes = protocol.MaxOutputDataLen
3031 graphicsUploadChunkBytes = 128 << 10
3132 )
3233
M internal/usecase/daemon/input_test.go +7 -6
@@ -17,6 +17,7 @@ import (
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
1919 portsmocks "github.com/bnema/vev/internal/ports/mocks"
20+ "github.com/bnema/vev/internal/protocol"
2021 scopy "github.com/bnema/vev/internal/usecase/copy"
2122 "github.com/bnema/vev/internal/usecase/keys"
2223 "github.com/bnema/vev/internal/usecase/layout"
@@ -789,18 +790,18 @@ func TestPaletteBackSessionSendsClientPreviousRouteAction(t *testing.T) {
789790 effect, admitted := ac.beginAttachmentEffect(token)
790791 require.True(t, admitted)
791792 defer effect.End()
792- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
793+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
793794 Generation: 3,
794- Active: ports.RouteRef{Key: 3, Generation: 3},
795- Previous: ports.RouteRef{Key: 2, Generation: 2},
796- Entries: []ports.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, ports.RouteKindLocal)},
795+ Active: protocol.RouteRef{Key: 3, Generation: 3},
796+ Previous: protocol.RouteRef{Key: 2, Generation: 2},
797+ Entries: []protocol.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, protocol.RouteKindLocal)},
797798 })
798799
799800 require.NoError(t, d.backSessionForAttachment(effect))
800801 frame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
801802 action, err := ports.UnmarshalRouteNavigationAction(frame.Payload)
802803 require.NoError(t, err)
803- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 3, Key: 2, Generation: 2}, action)
804+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 3, Key: 2, Generation: 2}, action)
804805 }
805806
806807 func TestBackSessionWithoutClientHistoryIsNoop(t *testing.T) {
@@ -1088,7 +1089,7 @@ func TestCopyModeMouseHorizontalReverseDragUsesExactOSC52(t *testing.T) {
10881089 mustOutputData(t, sends)
10891090 d.handleInput(sess, ac, []byte("\x1b[<0;2;2m"))
10901091 d.handleInput(sess, ac, []byte("y"))
1091- var msg ports.Output
1092+ var msg protocol.Output
10921093 require.Eventually(t, func() bool {
10931094 frame := awaitFrame(t, sends, ports.MsgOutput)
10941095 var err error
M internal/usecase/daemon/move_lifecycle_test.go +1 -2
@@ -4,9 +4,8 @@ import (
44 "testing"
55 "time"
66
7- "github.com/stretchr/testify/require"
8-
97 "github.com/bnema/vev/internal/ports"
8+ "github.com/stretchr/testify/require"
109 )
1110
1211 func awaitMoveTeardownState(t *testing.T, sess *session, reservations, waiters uint) {
M internal/usecase/daemon/output_state_sync_test.go +3 -2
@@ -12,6 +12,7 @@ import (
1212 renderer "github.com/bnema/vev-vt/ansi"
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 "github.com/stretchr/testify/require"
1617 )
1718
@@ -102,7 +103,7 @@ func TestDatagramAttachPipelinesRendererBeforeAck(t *testing.T) {
102103 require.Equal(t, uint64(2), out.New)
103104 // Both states were emitted before the MsgAck; the ordered dependency chain
104105 // must therefore remain valid without waiting for renderer acknowledgement.
105- tr.recv <- ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: out.Epoch, State: out.New})}
106+ tr.recv <- ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: out.Epoch, State: out.New})}
106107 require.NoError(t, tr.Close())
107108 d.runConnLoop(ac)
108109
@@ -197,7 +198,7 @@ func outputStateRow(row []renderer.Cell) string {
197198 return string(runes)
198199 }
199200
200-func mustApplyOutput(t *testing.T, screen *vt.Screen, frame ports.Frame) ports.Output {
201+func mustApplyOutput(t *testing.T, screen *vt.Screen, frame ports.Frame) protocol.Output {
201202 t.Helper()
202203 out, err := ports.UnmarshalOutput(frame.Payload)
203204 require.NoError(t, err)
M internal/usecase/daemon/overlay_runtime.go +4 -4
@@ -6,7 +6,7 @@ import (
66 "time"
77
88 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
1010 scopy "github.com/bnema/vev/internal/usecase/copy"
1111 "github.com/bnema/vev/internal/usecase/notices"
1212 "github.com/bnema/vev/internal/usecase/palette"
@@ -43,7 +43,7 @@ type overlayRuntime struct {
4343
4444 paletteMu sync.Mutex
4545 palette *palette.Model
46- paletteRouteSnapshot ports.RecentRouteSnapshot
46+ paletteRouteSnapshot protocol.RecentRouteSnapshot
4747 paletteGeneration uint64
4848 paletteHints palette.ContextualHints
4949 palettePreview string
@@ -309,7 +309,7 @@ type overlayRenderSnapshot struct {
309309 paletteHints *palette.ContextualHints
310310 palettePreview string
311311 paletteFeedback string
312- paletteRouteSnapshot ports.RecentRouteSnapshot
312+ paletteRouteSnapshot protocol.RecentRouteSnapshot
313313 paletteLocked bool
314314
315315 promptActive bool
@@ -383,7 +383,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot {
383383 snap.palettePreview = rt.palettePreview
384384 snap.paletteFeedback = rt.paletteFeedback
385385 snap.paletteRouteSnapshot = rt.paletteRouteSnapshot
386- snap.paletteRouteSnapshot.Entries = append([]ports.RecentRouteEntry(nil), rt.paletteRouteSnapshot.Entries...)
386+ snap.paletteRouteSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), rt.paletteRouteSnapshot.Entries...)
387387 snap.paletteLocked = true
388388 } else {
389389 rt.paletteMu.Unlock()
M internal/usecase/daemon/palette.go +25 -24
@@ -8,6 +8,7 @@ import (
88 renderer "github.com/bnema/vev-vt"
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 "github.com/bnema/vev/internal/usecase/command"
1213 "github.com/bnema/vev/internal/usecase/layout"
1314 "github.com/bnema/vev/internal/usecase/palette"
@@ -61,7 +62,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
6162 }
6263
6364 type paletteSessionIdentity struct {
64- kind ports.RouteKind
65+ kind protocol.RouteKind
6566 endpoint string
6667 lifecycle domain.SessionLifecycleID
6768 }
@@ -73,11 +74,11 @@ type remotePalettePresentationIdentity struct {
7374 }
7475
7576 func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity {
76- return paletteSessionIdentity{kind: ports.RouteKindLocal, endpoint: "local", lifecycle: lifecycle}
77+ return paletteSessionIdentity{kind: protocol.RouteKindLocal, endpoint: "local", lifecycle: lifecycle}
7778 }
7879
7980 func remotePaletteSessionIdentity(endpoint string, lifecycle domain.SessionLifecycleID) paletteSessionIdentity {
80- return paletteSessionIdentity{kind: ports.RouteKindRemote, endpoint: endpoint, lifecycle: lifecycle}
81+ return paletteSessionIdentity{kind: protocol.RouteKindRemote, endpoint: endpoint, lifecycle: lifecycle}
8182 }
8283
8384 func paletteRemoteDisplayOrigin(hostLabel string) string {
@@ -88,19 +89,19 @@ func paletteRemoteDisplayOrigin(hostLabel string) string {
8889 return origin
8990 }
9091
91-func paletteDaemonDisplayOrigin(snapshot ports.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) string {
92+func paletteDaemonDisplayOrigin(snapshot protocol.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) string {
9293 active, ok := activeRouteEntryForLifecycle(snapshot, lifecycle)
93- if !ok || active.Kind != ports.RouteKindRemote {
94+ if !ok || active.Kind != protocol.RouteKindRemote {
9495 return ""
9596 }
9697 return paletteRemoteDisplayOrigin(active.HostLabel)
9798 }
9899
99-func paletteRouteRepresentsDaemonSession(entry ports.RecentRouteEntry, daemonDisplayOrigin string) bool {
100- if entry.Kind == ports.RouteKindLocal {
100+func paletteRouteRepresentsDaemonSession(entry protocol.RecentRouteEntry, daemonDisplayOrigin string) bool {
101+ if entry.Kind == protocol.RouteKindLocal {
101102 return true
102103 }
103- return entry.Kind == ports.RouteKindRemote && daemonDisplayOrigin != "" &&
104+ return entry.Kind == protocol.RouteKindRemote && daemonDisplayOrigin != "" &&
104105 paletteRemoteDisplayOrigin(entry.HostLabel) == daemonDisplayOrigin
105106 }
106107
@@ -113,7 +114,7 @@ func remotePaletteUnavailableReason(status remoteHostStatus, session ports.Remot
113114
114115 // paletteResults captures eligible named sessions before paletteMu so the
115116 // palette has an immutable lifecycle target without violating lock ordering.
116-func (d *Daemon) paletteResults(current *session, commands []command.Command, routeSnapshot ports.RecentRouteSnapshot) []palette.Result {
117+func (d *Daemon) paletteResults(current *session, commands []command.Command, routeSnapshot protocol.RecentRouteSnapshot) []palette.Result {
117118 d.mu.Lock()
118119 sessions := d.sessionsSnapshotLocked()
119120 stopped := make([]inactiveSession, 0, len(d.inactive))
@@ -154,7 +155,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
154155 if candidate == current {
155156 continue
156157 }
157- target := ports.ExactSessionTarget{LifecycleID: snap.incarnation, SessionName: snap.name}
158+ target := protocol.ExactSessionTarget{LifecycleID: snap.incarnation, SessionName: snap.name}
158159 active = append(active, palette.NewActiveSessionResultWithDisplayOrigin(target, time.Unix(0, snap.createdAt), daemonDisplayOrigin))
159160 }
160161 sort.Slice(active, func(i, j int) bool {
@@ -165,7 +166,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
165166 sort.Slice(stopped, func(i, j int) bool { return stopped[i].name < stopped[j].name })
166167 results = append(results, active...)
167168 for _, candidate := range stopped {
168- target := ports.ExactSessionTarget{LifecycleID: candidate.incarnation, SessionName: candidate.name}
169+ target := protocol.ExactSessionTarget{LifecycleID: candidate.incarnation, SessionName: candidate.name}
169170 results = append(results, palette.NewStoppedSessionResultWithDisplayOrigin(target, time.Unix(0, candidate.createdAt), daemonDisplayOrigin))
170171 represented[localPaletteSessionIdentity(target.LifecycleID)] = struct{}{}
171172 }
@@ -210,12 +211,12 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
210211 if _, exists := represented[identity]; exists && paletteRouteRepresentsDaemonSession(entry, daemonDisplayOrigin) {
211212 continue
212213 }
213- results = append(results, palette.NewRecentRouteResult(label, ports.RouteNavigationAction{
214+ results = append(results, palette.NewRecentRouteResult(label, protocol.RouteNavigationAction{
214215 SnapshotGeneration: routeSnapshot.Generation,
215216 Key: entry.Key,
216217 Generation: entry.Generation,
217218 }))
218- if entry.Kind == ports.RouteKindLocal {
219+ if entry.Kind == protocol.RouteKindLocal {
219220 represented[identity] = struct{}{}
220221 continue
221222 }
@@ -251,7 +252,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
251252 model := rt.palette
252253 generation := rt.paletteGeneration
253254 routeSnapshot := rt.paletteRouteSnapshot
254- routeSnapshot.Entries = append([]ports.RecentRouteEntry(nil), routeSnapshot.Entries...)
255+ routeSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), routeSnapshot.Entries...)
255256 rt.paletteMu.Unlock()
256257 if model == nil {
257258 return
@@ -266,7 +267,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
266267 model.ReplaceResults(results)
267268 }
268269
269-func recentRouteHints(snapshot ports.RecentRouteSnapshot, args []string) palette.ContextualHints {
270+func recentRouteHints(snapshot protocol.RecentRouteSnapshot, args []string) palette.ContextualHints {
270271 formatted := formatRecentRouteSnapshot(snapshot)
271272 names := make([]string, len(formatted))
272273 for i, entry := range formatted {
@@ -342,10 +343,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...
342343 var remoteKey domain.RemoteSessionKey
343344 var remoteUnavailableReason string
344345 var hasRemoteTarget bool
345- var routeTarget ports.RouteNavigationAction
346+ var routeTarget protocol.RouteNavigationAction
346347 var hasRouteTarget bool
347348 var args []string
348- var routeSnapshot ports.RecentRouteSnapshot
349+ var routeSnapshot protocol.RecentRouteSnapshot
349350 var generation uint64
350351 var rawQuery string
351352 changed, cancel, execute := false, false, false
@@ -419,7 +420,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...
419420 }
420421 if cmd.ContextHint == command.ContextHintRecentSessions {
421422 routeSnapshot = ac.overlays.paletteRouteSnapshot
422- routeSnapshot.Entries = append([]ports.RecentRouteEntry(nil), routeSnapshot.Entries...)
423+ routeSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), routeSnapshot.Entries...)
423424 }
424425 } else if target, ok := selected.RemoteSessionTarget(); ok {
425426 key, keyOK := selected.RemoteSessionKey()
@@ -624,7 +625,7 @@ func (ac *attachedClient) clearPaletteLocked() remoteDiscoveryInstance {
624625 model := ac.overlays.palette
625626 ac.overlays.paletteGeneration++
626627 ac.overlays.palette = nil
627- ac.overlays.paletteRouteSnapshot = ports.RecentRouteSnapshot{}
628+ ac.overlays.paletteRouteSnapshot = protocol.RecentRouteSnapshot{}
628629 ac.overlays.paletteHints = palette.ContextualHints{}
629630 ac.overlays.palettePreview = ""
630631 ac.overlays.paletteFeedback = ""
@@ -679,7 +680,7 @@ type paletteExec struct {
679680 sess *session
680681 attachment *session
681682 ac *attachedClient
682- routeSnapshot ports.RecentRouteSnapshot
683+ routeSnapshot protocol.RecentRouteSnapshot
683684 actions daemonActionRunner
684685 effect *attachmentEffect
685686 redrawClosedPalette bool
@@ -911,8 +912,8 @@ func (e paletteExec) RenameTabTo(name string) error {
911912 }
912913
913914 func (e paletteExec) OpenSessionPicker() error {
914- if e.ac != nil && e.ac.navigationCapabilities&ports.NavigationCapabilityHomePicker != 0 && e.effect != nil {
915- return e.d.sendNavigationActionForAttachment(e.effect, ports.NavigationOpenHomePicker)
915+ if e.ac != nil && e.ac.navigationCapabilities&protocol.NavigationCapabilityHomePicker != 0 && e.effect != nil {
916+ return e.d.sendNavigationActionForAttachment(e.effect, protocol.NavigationOpenHomePicker)
916917 }
917918 e.d.enterPicker(e.sess, e.ac)
918919 return nil
@@ -945,14 +946,14 @@ func (e paletteExec) JumpRecentSession(rank int) error {
945946 return command.ErrInvalidArguments
946947 }
947948 entry := e.routeSnapshot.Entries[rank-1]
948- return e.NavigateRecentRoute(ports.RouteNavigationAction{
949+ return e.NavigateRecentRoute(protocol.RouteNavigationAction{
949950 SnapshotGeneration: e.routeSnapshot.Generation,
950951 Key: entry.Key,
951952 Generation: entry.Generation,
952953 })
953954 }
954955
955-func (e paletteExec) NavigateRecentRoute(action ports.RouteNavigationAction) error {
956+func (e paletteExec) NavigateRecentRoute(action protocol.RouteNavigationAction) error {
956957 if e.d.beforeRecentSessionHandoff != nil {
957958 e.d.beforeRecentSessionHandoff()
958959 }
M internal/usecase/daemon/palette_test.go +54 -53
@@ -13,6 +13,7 @@ import (
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
1515 portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
1617 "github.com/bnema/vev/internal/usecase/command"
1718 "github.com/bnema/vev/internal/usecase/palette"
1819 "github.com/bnema/vev/internal/usecase/picker"
@@ -20,14 +21,14 @@ import (
2021 "github.com/bnema/vev/internal/usecase/ui"
2122 )
2223
23-func testRecentRouteSnapshot() ports.RecentRouteSnapshot {
24- return ports.RecentRouteSnapshot{
24+func testRecentRouteSnapshot() protocol.RecentRouteSnapshot {
25+ return protocol.RecentRouteSnapshot{
2526 Generation: 1,
26- Active: ports.RouteRef{Key: 1, Generation: 1},
27- ActiveEntry: testRouteEntry(1, 1, "current", 1, ports.RouteKindLocal),
28- Entries: []ports.RecentRouteEntry{
29- testRouteEntry(2, 1, "recent", 2, ports.RouteKindLocal),
30- testRouteEntry(3, 1, "older", 3, ports.RouteKindLocal),
27+ Active: protocol.RouteRef{Key: 1, Generation: 1},
28+ ActiveEntry: testRouteEntry(1, 1, "current", 1, protocol.RouteKindLocal),
29+ Entries: []protocol.RecentRouteEntry{
30+ testRouteEntry(2, 1, "recent", 2, protocol.RouteKindLocal),
31+ testRouteEntry(3, 1, "older", 3, protocol.RouteKindLocal),
3132 },
3233 }
3334 }
@@ -232,10 +233,10 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
232233 d.inactive["purging"] = inactiveSession{name: "purging", createdAt: 13, incarnation: domain.IncarnationID{4}, state: ports.SessionDown, purging: true}
233234 d.inactive["broken"] = inactiveSession{name: "broken", createdAt: 14, incarnation: domain.IncarnationID{5}, state: ports.SessionBroken}
234235 d.inactive["degraded"] = inactiveSession{name: "degraded", createdAt: 15, incarnation: domain.IncarnationID{6}, state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}
235- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1, Entries: []ports.RecentRouteEntry{
236- testRouteEntry(1, 1, "purging", 4, ports.RouteKindLocal),
237- testRouteEntry(2, 1, "broken", 5, ports.RouteKindLocal),
238- testRouteEntry(3, 1, "degraded", 6, ports.RouteKindLocal),
236+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Entries: []protocol.RecentRouteEntry{
237+ testRouteEntry(1, 1, "purging", 4, protocol.RouteKindLocal),
238+ testRouteEntry(2, 1, "broken", 5, protocol.RouteKindLocal),
239+ testRouteEntry(3, 1, "degraded", 6, protocol.RouteKindLocal),
239240 }})
240241
241242 d.enterPalette(current, ac)
@@ -374,7 +375,7 @@ func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
374375 p, release := newBlockingPTY(t)
375376 defer release()
376377 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
377- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1, Active: ports.RouteRef{Key: 1, Generation: 1}})
378+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Active: protocol.RouteRef{Key: 1, Generation: 1}})
378379
379380 d.handleInput(sess, ac, []byte("\x1b "))
380381 awaitFrame(t, sends, ports.MsgOutput)
@@ -412,7 +413,7 @@ func TestPaletteJRSUsesEffectiveOverrideOnly(t *testing.T) {
412413 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
413414 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
414415 require.NoError(t, err)
415- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
416+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
416417 require.Same(t, current, ac.currentSession())
417418 require.False(t, ac.overlays.paletteActive())
418419
@@ -447,7 +448,7 @@ func TestPaletteIncludesExactRemoteCatalogTargetBesideSameNameLocalSession(t *te
447448 d.remoteCatalog.status["user@arch"] = remoteHostFresh
448449 d.remoteCatalog.mu.Unlock()
449450
450- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{})
451+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{})
451452 var matching []palette.Result
452453 for _, result := range results {
453454 if result.DisplayText() == "Switch to session vev" || result.DisplayText() == "Switch to session vev@arch" {
@@ -472,18 +473,18 @@ func TestPaletteQualifiesDaemonSessionForRemoteAttachment(t *testing.T) {
472473 target.ephemeral = false
473474 target.incarnation = domain.SessionLifecycleID{42}
474475
475- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{
476+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{
476477 Generation: 1,
477- Active: ports.RouteRef{Key: 1, Generation: 1},
478- ActiveEntry: ports.RecentRouteEntry{
478+ Active: protocol.RouteRef{Key: 1, Generation: 1},
479+ ActiveEntry: protocol.RecentRouteEntry{
479480 Key: 1, Generation: 1,
480- Target: ports.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "current"},
481- Name: "current", HostLabel: "user@remote-host", Kind: ports.RouteKindRemote,
481+ Target: protocol.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "current"},
482+ Name: "current", HostLabel: "user@remote-host", Kind: protocol.RouteKindRemote,
482483 },
483- Entries: []ports.RecentRouteEntry{{
484+ Entries: []protocol.RecentRouteEntry{{
484485 Key: 2, Generation: 1,
485- Target: ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"},
486- Name: "target", HostLabel: "user@remote-host", Kind: ports.RouteKindRemote,
486+ Target: protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"},
487+ Name: "target", HostLabel: "user@remote-host", Kind: protocol.RouteKindRemote,
487488 }},
488489 })
489490
@@ -496,7 +497,7 @@ func TestPaletteQualifiesDaemonSessionForRemoteAttachment(t *testing.T) {
496497 require.Len(t, matching, 1)
497498 got, ok := matching[0].SessionTarget()
498499 require.True(t, ok)
499- require.Equal(t, ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"}, got)
500+ require.Equal(t, protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"}, got)
500501 }
501502
502503 func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
@@ -536,15 +537,15 @@ func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
536537 }
537538 d.remoteCatalog.mu.Unlock()
538539
539- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{
540+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{
540541 Generation: 2,
541- Entries: []ports.RecentRouteEntry{{
542+ Entries: []protocol.RecentRouteEntry{{
542543 Key: 3, Generation: 1,
543- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"},
544- Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote,
544+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"},
545+ Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote,
545546 }},
546547 })
547- var actions []ports.RouteNavigationAction
548+ var actions []protocol.RouteNavigationAction
548549 var catalogEndpoints []string
549550 for _, result := range results {
550551 if result.DisplayText() != "Switch to session vev@arch" {
@@ -558,7 +559,7 @@ func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
558559 require.True(t, ok)
559560 catalogEndpoints = append(catalogEndpoints, target.Endpoint)
560561 }
561- require.Equal(t, []ports.RouteNavigationAction{{
562+ require.Equal(t, []protocol.RouteNavigationAction{{
562563 SnapshotGeneration: 2, Key: 3, Generation: 1,
563564 }}, actions)
564565 require.ElementsMatch(t, test.wantCatalogEndpoints, catalogEndpoints)
@@ -720,21 +721,21 @@ func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) {
720721 local := addControlSession(d, "vev", "tab-2", "pane-2")
721722 local.ephemeral = false
722723 local.incarnation = domain.SessionLifecycleID{2}
723- remoteOne := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{21}, SessionName: "vev"}
724- remoteTwo := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{22}, SessionName: "vev"}
725- snapshot := ports.RecentRouteSnapshot{
724+ remoteOne := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{21}, SessionName: "vev"}
725+ remoteTwo := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{22}, SessionName: "vev"}
726+ snapshot := protocol.RecentRouteSnapshot{
726727 Generation: 2,
727- Entries: []ports.RecentRouteEntry{
728- {Key: 1, Generation: 1, Target: ports.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "hi"}, Name: "hi", Kind: ports.RouteKindLocal},
729- {Key: 2, Generation: 1, Target: ports.ExactSessionTarget{LifecycleID: local.incarnation, SessionName: "vev"}, Name: "vev", Kind: ports.RouteKindLocal},
730- {Key: 3, Generation: 1, Target: remoteOne, Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote},
731- {Key: 4, Generation: 1, Target: remoteTwo, Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote},
728+ Entries: []protocol.RecentRouteEntry{
729+ {Key: 1, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "hi"}, Name: "hi", Kind: protocol.RouteKindLocal},
730+ {Key: 2, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: local.incarnation, SessionName: "vev"}, Name: "vev", Kind: protocol.RouteKindLocal},
731+ {Key: 3, Generation: 1, Target: remoteOne, Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote},
732+ {Key: 4, Generation: 1, Target: remoteTwo, Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote},
732733 },
733734 }
734735
735736 results := d.paletteResults(current, nil, snapshot)
736- var sessionTargets []ports.ExactSessionTarget
737- var routeActions []ports.RouteNavigationAction
737+ var sessionTargets []protocol.ExactSessionTarget
738+ var routeActions []protocol.RouteNavigationAction
738739 for _, result := range results {
739740 if target, ok := result.SessionTarget(); ok {
740741 sessionTargets = append(sessionTargets, target)
@@ -743,8 +744,8 @@ func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) {
743744 routeActions = append(routeActions, action)
744745 }
745746 }
746- require.Equal(t, []ports.ExactSessionTarget{{LifecycleID: local.incarnation, SessionName: "vev"}}, sessionTargets)
747- require.Equal(t, []ports.RouteNavigationAction{
747+ require.Equal(t, []protocol.ExactSessionTarget{{LifecycleID: local.incarnation, SessionName: "vev"}}, sessionTargets)
748+ require.Equal(t, []protocol.RouteNavigationAction{
748749 {SnapshotGeneration: 2, Key: 3, Generation: 1},
749750 {SnapshotGeneration: 2, Key: 4, Generation: 1},
750751 }, routeActions)
@@ -781,12 +782,12 @@ func TestPaletteLifecycleTargetRejectsSameNameReplacement(t *testing.T) {
781782 func TestPaletteFuzzyRemoteRecentRouteSendsExactNavigationAction(t *testing.T) {
782783 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
783784 defer releaseAll(releases)
784- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
785+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
785786 Generation: 9,
786- Active: ports.RouteRef{Key: 1, Generation: 1},
787- ActiveEntry: testRouteEntry(1, 1, current.name, 1, ports.RouteKindLocal),
788- Entries: []ports.RecentRouteEntry{{
789- Key: 8, Generation: 4, Target: testRouteTarget("logs", 8), Name: "logs", HostLabel: "edge", Kind: ports.RouteKindRemote,
787+ Active: protocol.RouteRef{Key: 1, Generation: 1},
788+ ActiveEntry: testRouteEntry(1, 1, current.name, 1, protocol.RouteKindLocal),
789+ Entries: []protocol.RecentRouteEntry{{
790+ Key: 8, Generation: 4, Target: testRouteTarget("logs", 8), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,
790791 }},
791792 })
792793 token := beginRecentRoutePaletteEffect(t, d, current, ac)
@@ -798,7 +799,7 @@ func TestPaletteFuzzyRemoteRecentRouteSendsExactNavigationAction(t *testing.T) {
798799 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
799800 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
800801 require.NoError(t, err)
801- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 9, Key: 8, Generation: 4}, action)
802+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 9, Key: 8, Generation: 4}, action)
802803 require.False(t, ac.overlays.paletteActive())
803804 }
804805
@@ -830,7 +831,7 @@ func TestPaletteJRSUsesCapturedRankAfterMRUChanges(t *testing.T) {
830831 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
831832 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
832833 require.NoError(t, err)
833- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
834+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
834835 require.Same(t, current, ac.currentSession())
835836 require.False(t, ac.overlays.paletteActive())
836837 }
@@ -861,11 +862,11 @@ func TestPaletteJRSDoesNotRevalidateTargetInDaemon(t *testing.T) {
861862 p, release := newBlockingPTY(t)
862863 defer release()
863864 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
864- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
865+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
865866 Generation: 7,
866- Active: ports.RouteRef{Key: 1, Generation: 1},
867- ActiveEntry: testRouteEntry(1, 1, sess.name, 1, ports.RouteKindLocal),
868- Entries: []ports.RecentRouteEntry{testRouteEntry(9, 4, "captured", 9, ports.RouteKindLocal)},
867+ Active: protocol.RouteRef{Key: 1, Generation: 1},
868+ ActiveEntry: testRouteEntry(1, 1, sess.name, 1, protocol.RouteKindLocal),
869+ Entries: []protocol.RecentRouteEntry{testRouteEntry(9, 4, "captured", 9, protocol.RouteKindLocal)},
869870 })
870871
871872 validated := make(chan struct{})
@@ -891,7 +892,7 @@ func TestPaletteJRSDoesNotRevalidateTargetInDaemon(t *testing.T) {
891892 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
892893 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
893894 require.NoError(t, err)
894- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 7, Key: 9, Generation: 4}, action)
895+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 7, Key: 9, Generation: 4}, action)
895896 require.False(t, ac.overlays.paletteActive())
896897 }
897898
M internal/usecase/daemon/parked_route.go +52 -51
@@ -8,12 +8,13 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 const parkedRouteLeaseTTL = 15 * time.Minute
1415
1516 type parkedRouteLease struct {
16- id ports.ParkedRouteLeaseID
17+ id protocol.ParkedRouteLeaseID
1718 generation uint64
1819 transport transportSnapshot
1920 expiresAt time.Time
@@ -24,14 +25,14 @@ type parkedRouteLease struct {
2425 expired bool
2526 }
2627
27-func (lease *parkedRouteLease) matches(id ports.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {
28+func (lease *parkedRouteLease) matches(id protocol.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {
2829 return lease != nil && lease.id == id && lease.generation == generation && lease.transport == transport
2930 }
3031
31-func newParkedRouteLeaseID() (ports.ParkedRouteLeaseID, error) {
32- var id ports.ParkedRouteLeaseID
32+func newParkedRouteLeaseID() (protocol.ParkedRouteLeaseID, error) {
33+ var id protocol.ParkedRouteLeaseID
3334 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
34- return ports.ParkedRouteLeaseID{}, err
35+ return protocol.ParkedRouteLeaseID{}, err
3536 }
3637 return id, nil
3738 }
@@ -50,17 +51,17 @@ func stopParkedRouteLeaseLocked(lease *parkedRouteLease) {
5051 }
5152 }
5253
53-func (d *Daemon) armParkedRoute(effect *attachmentEffect) (ports.ParkedRouteLeaseID, error) {
54+func (d *Daemon) armParkedRoute(effect *attachmentEffect) (protocol.ParkedRouteLeaseID, error) {
5455 if !effect.current() {
55- return ports.ParkedRouteLeaseID{}, errAttachmentTransition
56+ return protocol.ParkedRouteLeaseID{}, errAttachmentTransition
5657 }
5758 ac := effect.ac
58- if ac == nil || ac.navigationCapabilities&ports.NavigationCapabilityHomePicker == 0 {
59- return ports.ParkedRouteLeaseID{}, errAttachmentTransition
59+ if ac == nil || ac.navigationCapabilities&protocol.NavigationCapabilityHomePicker == 0 {
60+ return protocol.ParkedRouteLeaseID{}, errAttachmentTransition
6061 }
6162 id, err := newParkedRouteLeaseID()
6263 if err != nil {
63- return ports.ParkedRouteLeaseID{}, err
64+ return protocol.ParkedRouteLeaseID{}, err
6465 }
6566 lease := &parkedRouteLease{
6667 id: id, generation: effect.generation, transport: effect.transport,
@@ -123,30 +124,30 @@ func (ac *attachedClient) clearParkedRoute() {
123124 ac.parkedRouteMu.Unlock()
124125 }
125126
126-func (ac *attachedClient) parkedRouteStatus(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time, suspended bool) ports.ParkedRouteStatus {
127+func (ac *attachedClient) parkedRouteStatus(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time, suspended bool) protocol.ParkedRouteStatus {
127128 if ac == nil || !token.current() {
128- return ports.ParkedRouteRejected
129+ return protocol.ParkedRouteRejected
129130 }
130131 ac.parkedRouteMu.Lock()
131132 defer ac.parkedRouteMu.Unlock()
132133 lease := ac.parkedRoute
133134 if !lease.matches(id, token.generation, token.transport) {
134- return ports.ParkedRouteRejected
135+ return protocol.ParkedRouteRejected
135136 }
136137 if !now.Before(lease.expiresAt) {
137138 stopParkedRouteLeaseLocked(lease)
138139 ac.parkedRoute = nil
139140 ac.parkedRouteOutput.Store(false)
140141 ac.parkedRouteFullPending.Store(false)
141- return ports.ParkedRouteExpired
142+ return protocol.ParkedRouteExpired
142143 }
143144 if lease.suspended != suspended {
144- return ports.ParkedRouteRejected
145+ return protocol.ParkedRouteRejected
145146 }
146147 return 0
147148 }
148149
149-func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {
150+func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
150151 if status := ac.parkedRouteStatus(token, id, now, false); status != 0 {
151152 return status
152153 }
@@ -154,14 +155,14 @@ func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id port
154155 defer ac.parkedRouteMu.Unlock()
155156 lease := ac.parkedRoute
156157 if !lease.matches(id, token.generation, token.transport) || lease.suspended {
157- return ports.ParkedRouteRejected
158+ return protocol.ParkedRouteRejected
158159 }
159160 lease.suspended = true
160161 ac.parkedRouteOutput.Store(true)
161162 return 0
162163 }
163164
164-func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {
165+func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
165166 if status := ac.parkedRouteStatus(token, id, now, true); status != 0 {
166167 return status
167168 }
@@ -169,43 +170,43 @@ func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id port
169170 defer ac.parkedRouteMu.Unlock()
170171 lease := ac.parkedRoute
171172 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
172- return ports.ParkedRouteRejected
173+ return protocol.ParkedRouteRejected
173174 }
174175 stopParkedRouteLeaseLocked(lease)
175176 ac.parkedRoute = nil
176177 return 0
177178 }
178179
179-func (ac *attachedClient) beginParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {
180+func (ac *attachedClient) beginParkedRouteSwitch(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
180181 if ac == nil || !token.current() {
181- return ports.ParkedRouteRejected
182+ return protocol.ParkedRouteRejected
182183 }
183184 ac.parkedRouteMu.Lock()
184185 defer ac.parkedRouteMu.Unlock()
185186 lease := ac.parkedRoute
186187 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
187- return ports.ParkedRouteRejected
188+ return protocol.ParkedRouteRejected
188189 }
189190 if lease.expired || !now.Before(lease.expiresAt) {
190191 stopParkedRouteLeaseLocked(lease)
191192 ac.parkedRoute = nil
192193 ac.parkedRouteOutput.Store(false)
193194 ac.parkedRouteFullPending.Store(false)
194- return ports.ParkedRouteExpired
195+ return protocol.ParkedRouteExpired
195196 }
196197 lease.inFlight = true
197198 return 0
198199 }
199200
200-func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {
201+func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
201202 if ac == nil {
202- return ports.ParkedRouteRejected
203+ return protocol.ParkedRouteRejected
203204 }
204205 ac.parkedRouteMu.Lock()
205206 defer ac.parkedRouteMu.Unlock()
206207 lease := ac.parkedRoute
207208 if !lease.matches(id, token.generation, token.transport) || !lease.inFlight {
208- return ports.ParkedRouteRejected
209+ return protocol.ParkedRouteRejected
209210 }
210211 lease.inFlight = false
211212 if lease.expired || !now.Before(lease.expiresAt) {
@@ -213,20 +214,20 @@ func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id
213214 ac.parkedRoute = nil
214215 ac.parkedRouteOutput.Store(false)
215216 ac.parkedRouteFullPending.Store(false)
216- return ports.ParkedRouteExpired
217+ return protocol.ParkedRouteExpired
217218 }
218219 return 0
219220 }
220221
221-func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentCapability, id ports.ParkedRouteLeaseID) ports.ParkedRouteStatus {
222+func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentCapability, id protocol.ParkedRouteLeaseID) protocol.ParkedRouteStatus {
222223 if ac == nil || published.ac != ac || !published.current() {
223- return ports.ParkedRouteRejected
224+ return protocol.ParkedRouteRejected
224225 }
225226 ac.parkedRouteMu.Lock()
226227 defer ac.parkedRouteMu.Unlock()
227228 lease := ac.parkedRoute
228229 if !lease.matches(id, previous.generation, previous.transport) || !lease.suspended || !lease.inFlight {
229- return ports.ParkedRouteRejected
230+ return protocol.ParkedRouteRejected
230231 }
231232 // Expiry is checked when the request is admitted. Once publication starts,
232233 // the accepted request must commit atomically instead of turning a completed
@@ -236,7 +237,7 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
236237 return 0
237238 }
238239
239-func parkedRouteResponseFrame(response ports.ParkedRouteResponse) (ports.Frame, error) {
240+func parkedRouteResponseFrame(response protocol.ParkedRouteResponse) (ports.Frame, error) {
240241 payload := ports.MarshalParkedRouteResponse(response)
241242 if payload == nil {
242243 return ports.Frame{}, errAttachmentTransition
@@ -244,7 +245,7 @@ func parkedRouteResponseFrame(response ports.ParkedRouteResponse) (ports.Frame,
244245 return ports.Frame{Type: ports.MsgParkedRouteResponse, Payload: payload}, nil
245246 }
246247
247-func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response ports.ParkedRouteResponse) error {
248+func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response protocol.ParkedRouteResponse) error {
248249 frame, err := parkedRouteResponseFrame(response)
249250 if err != nil {
250251 return err
@@ -254,7 +255,7 @@ func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response port
254255
255256 // sendParkedRouteResponseLocked preserves response -> full-output ordering.
256257 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.
257-func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response ports.ParkedRouteResponse) error {
258+func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response protocol.ParkedRouteResponse) error {
258259 frame, err := parkedRouteResponseFrame(response)
259260 if err != nil {
260261 return err
@@ -283,7 +284,7 @@ func (d *Daemon) parkedRouteResponseFailed(effect *attachmentEffect) {
283284 launch()
284285 }
285286
286-func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response ports.ParkedRouteResponse) bool {
287+func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response protocol.ParkedRouteResponse) bool {
287288 if effect == nil || effect.ac == nil {
288289 return false
289290 }
@@ -307,35 +308,35 @@ func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response ports.Par
307308 return true
308309 }
309310
310-func (d *Daemon) handleParkedRouteRequest(effect *attachmentEffect, request ports.ParkedRouteRequest) {
311- if ports.ValidateParkedRouteRequest(request) != nil || !effect.current() || effect.ac == nil {
311+func (d *Daemon) handleParkedRouteRequest(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
312+ if protocol.ValidateParkedRouteRequest(request) != nil || !effect.current() || effect.ac == nil {
312313 return
313314 }
314315 capability := effect.capability()
315316 switch request.Action {
316- case ports.ParkedRoutePrepare:
317+ case protocol.ParkedRoutePrepare:
317318 status := effect.ac.prepareParkedRoute(capability, request.LeaseID, d.clock.Now())
318319 if status == 0 {
319- status = ports.ParkedRouteReady
320+ status = protocol.ParkedRouteReady
320321 }
321- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
322- case ports.ParkedRouteResume:
322+ d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
323+ case protocol.ParkedRouteResume:
323324 d.resumeParkedRoute(effect, request)
324- case ports.ParkedRouteSwitch:
325+ case protocol.ParkedRouteSwitch:
325326 d.switchParkedRoute(effect, request)
326327 }
327328 }
328329
329-func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) {
330+func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
330331 capability := effect.capability()
331332 status := effect.ac.consumeParkedRoute(capability, request.LeaseID, d.clock.Now())
332333 if status != 0 {
333- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
334+ d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
334335 return
335336 }
336337 ac := effect.ac
337338 ac.sendMu.Lock()
338- err := d.sendParkedRouteResponseLocked(ac, effect.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteResumed})
339+ err := d.sendParkedRouteResponseLocked(ac, effect.transport, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: protocol.ParkedRouteResumed})
339340 if err == nil {
340341 ac.releaseParkedOutputLocked()
341342 }
@@ -349,21 +350,21 @@ func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request ports.Parke
349350 }
350351 }
351352
352-func (d *Daemon) rejectParkedRouteSwitch(effect *attachmentEffect, request ports.ParkedRouteRequest, fallback ports.ParkedRouteStatus) {
353+func (d *Daemon) rejectParkedRouteSwitch(effect *attachmentEffect, request protocol.ParkedRouteRequest, fallback protocol.ParkedRouteStatus) {
353354 capability := effect.capability()
354355 status := effect.ac.rejectParkedRouteSwitch(capability, request.LeaseID, d.clock.Now())
355356 if status == 0 {
356357 status = fallback
357358 }
358- if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired {
359+ if d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == protocol.ParkedRouteExpired {
359360 _ = effect.ac.closeCapturedTransport(effect.transport.transport)
360361 }
361362 }
362363
363-func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) {
364+func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
364365 capability := effect.capability()
365366 if status := effect.ac.beginParkedRouteSwitch(capability, request.LeaseID, d.clock.Now()); status != 0 {
366- if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired {
367+ if d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == protocol.ParkedRouteExpired {
367368 _ = effect.ac.closeCapturedTransport(effect.transport.transport)
368369 }
369370 return
@@ -374,13 +375,13 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.Parke
374375 ctx = context.Background()
375376 }
376377 if err := d.waitForParkedTargetRestore(ctx, target.SessionName); err != nil {
377- d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget)
378+ d.rejectParkedRouteSwitch(effect, request, protocol.ParkedRouteStaleTarget)
378379 return
379380 }
380381
381382 transition, targetSession, err := d.transitionParkedRouteTarget(effect, target)
382383 if err != nil {
383- d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget)
384+ d.rejectParkedRouteSwitch(effect, request, protocol.ParkedRouteStaleTarget)
384385 return
385386 }
386387 if status := effect.ac.consumePublishedParkedRoute(capability, transition.published, request.LeaseID); status != 0 {
@@ -395,7 +396,7 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.Parke
395396 freshCapability := fresh.capability()
396397 ac := effect.ac
397398 ac.sendMu.Lock()
398- responseErr := d.sendParkedRouteResponseLocked(ac, freshCapability.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteSwitched})
399+ responseErr := d.sendParkedRouteResponseLocked(ac, freshCapability.transport, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: protocol.ParkedRouteSwitched})
399400 if responseErr == nil {
400401 ac.releaseParkedOutputLocked()
401402 }
M internal/usecase/daemon/parked_route_test.go +36 -35
@@ -13,6 +13,7 @@ import (
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
1515 portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
1617 )
1718
1819 type parkedRouteFailTransport struct{}
@@ -61,31 +62,31 @@ func (c *parkedRouteClock) setNow(now time.Time) {
6162 c.mu.Unlock()
6263 }
6364
64-func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) ports.ParkedRouteLeaseID {
65+func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) protocol.ParkedRouteLeaseID {
6566 t.Helper()
66- ac.navigationCapabilities |= ports.NavigationCapabilityHomePicker
67+ ac.navigationCapabilities |= protocol.NavigationCapabilityHomePicker
6768 token := source.captureAttachmentCapability(ac, ac.transport())
6869 effect, admitted := ac.beginAttachmentEffect(token)
6970 require.True(t, admitted)
70- require.NoError(t, d.sendNavigationActionForAttachment(effect, ports.NavigationOpenHomePicker))
71+ require.NoError(t, d.sendNavigationActionForAttachment(effect, protocol.NavigationOpenHomePicker))
7172 effect.End()
7273 directiveFrame := awaitFrame(t, sends, ports.MsgNavigationAction)
7374 directive, err := ports.UnmarshalNavigationDirective(directiveFrame.Payload)
7475 require.NoError(t, err)
7576
76- prepare := ports.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: ports.ParkedRoutePrepare}
77+ prepare := protocol.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: protocol.ParkedRoutePrepare}
7778 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(prepare)}))
7879 readyFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
7980 ready, err := ports.UnmarshalParkedRouteResponse(readyFrame.Payload)
8081 require.NoError(t, err)
81- require.Equal(t, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady}, ready)
82+ require.Equal(t, protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}, ready)
8283 return directive.LeaseID
8384 }
8485
8586 func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
8687 d, source, ac, sends, releases := newManualTabSession(t, 1)
8788 defer releaseAll(releases)
88- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
89+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
8990
9091 targetLifecycle := domain.SessionLifecycleID{7}
9192 target := &session{
@@ -97,7 +98,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
9798 d.mu.Lock()
9899 d.sessions[target.id] = target
99100 d.mu.Unlock()
100- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
101+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
101102
102103 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
103104 require.True(t, ac.parkedRouteOutput.Load())
@@ -106,7 +107,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
106107 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,
107108 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),
108109 }
109- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest}
110+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &targetRequest}
110111 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}))
111112
112113 require.Same(t, target, ac.currentAttachmentSession())
@@ -114,7 +115,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
114115 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
115116 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
116117 require.NoError(t, err)
117- require.Equal(t, ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteSwitched}, response)
118+ require.Equal(t, protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}, response)
118119 }
119120
120121 func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.T) {
@@ -122,7 +123,7 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
122123 defer releaseAll(releases)
123124 clock := &parkedRouteClock{}
124125 d.clock = clock
125- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
126+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
126127
127128 targetLifecycle := domain.SessionLifecycleID{7}
128129 target := &session{
@@ -134,7 +135,7 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
134135 d.mu.Lock()
135136 d.sessions[target.id] = target
136137 d.mu.Unlock()
137- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
138+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
138139
139140 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
140141 d.afterAttachmentEffectsFrozen = func() {
@@ -145,21 +146,21 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
145146 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,
146147 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),
147148 }
148- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest}
149+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &targetRequest}
149150 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
150151
151152 require.Same(t, target, ac.currentAttachmentSession())
152153 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
153154 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
154155 require.NoError(t, err)
155- require.Equal(t, ports.ParkedRouteSwitched, response.Status)
156+ require.Equal(t, protocol.ParkedRouteSwitched, response.Status)
156157 }
157158
158159 func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T) {
159160 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
160161 d.baseEnv = []string{"OWNER=daemon"}
161162 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")
162- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
163+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
163164 source.mu.Lock()
164165 source.env = []string{"OWNER=source"}
165166 source.mu.Unlock()
@@ -175,7 +176,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
175176 },
176177 }
177178 d.mu.Unlock()
178- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
179+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
179180
180181 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
181182
@@ -183,7 +184,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
183184 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: lifecycle,
184185 SessionName: "stopped", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-b"),
185186 }
186- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &target}
187+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &target}
187188 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
188189
189190 restored := ac.currentAttachmentSession()
@@ -199,8 +200,8 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
199200 func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
200201 d, source, ac, sends, releases := newManualTabSession(t, 1)
201202 defer releaseAll(releases)
202- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
203- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
203+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
204+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
204205
205206 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
206207
@@ -208,21 +209,21 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
208209 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{9},
209210 SessionName: "missing", LiveTabID: "tab-1",
210211 }
211- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &missing}
212+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &missing}
212213 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
213214 staleFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
214215 stale, err := ports.UnmarshalParkedRouteResponse(staleFrame.Payload)
215216 require.NoError(t, err)
216- require.Equal(t, ports.ParkedRouteStaleTarget, stale.Status)
217+ require.Equal(t, protocol.ParkedRouteStaleTarget, stale.Status)
217218 require.Same(t, source, ac.currentAttachmentSession())
218219 require.True(t, ac.parkedRouteOutput.Load())
219220
220- resume := ports.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: ports.ParkedRouteResume}
221+ resume := protocol.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
221222 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
222223 resumedFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
223224 resumed, err := ports.UnmarshalParkedRouteResponse(resumedFrame.Payload)
224225 require.NoError(t, err)
225- require.Equal(t, ports.ParkedRouteResumed, resumed.Status)
226+ require.Equal(t, protocol.ParkedRouteResumed, resumed.Status)
226227 require.False(t, ac.parkedRouteOutput.Load())
227228 outputFrame := awaitFrame(t, sends, ports.MsgOutput)
228229 output, err := ports.UnmarshalOutput(outputFrame.Payload)
@@ -233,7 +234,7 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
233234 func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
234235 d, source, ac, sends, releases := newManualTabSession(t, 1)
235236 defer releaseAll(releases)
236- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
237+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
237238
238239 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
239240 require.NoError(t, d.boundedSendOutputErr(ac, []byte("one-shot effect")))
@@ -243,7 +244,7 @@ func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
243244 default:
244245 }
245246
246- resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume}
247+ resume := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
247248 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
248249 _ = awaitFrame(t, sends, ports.MsgParkedRouteResponse)
249250 outputFrame := awaitFrame(t, sends, ports.MsgOutput)
@@ -261,19 +262,19 @@ func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
261262 func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
262263 d, source, ac, sends, releases := newManualTabSession(t, 1)
263264 defer releaseAll(releases)
264- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
265+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
265266
266267 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
267268
268269 ac.parkedRouteMu.Lock()
269270 ac.parkedRoute.expiresAt = d.clock.Now()
270271 ac.parkedRouteMu.Unlock()
271- resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume}
272+ resume := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
272273 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
273274 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
274275 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
275276 require.NoError(t, err)
276- require.Equal(t, ports.ParkedRouteExpired, response.Status)
277+ require.Equal(t, protocol.ParkedRouteExpired, response.Status)
277278 require.Same(t, source, ac.currentAttachmentSession())
278279 require.False(t, ac.parkedRouteOutput.Load())
279280 }
@@ -281,7 +282,7 @@ func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
281282 func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
282283 d, source, ac, sends, releases := newManualTabSession(t, 1)
283284 defer releaseAll(releases)
284- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
285+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
285286 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
286287
287288 ac.parkedRouteMu.Lock()
@@ -289,7 +290,7 @@ func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
289290 ac.parkedRouteMu.Unlock()
290291 status := ac.consumeParkedRoute(source.captureAttachmentCapability(ac, ac.transport()), leaseID, d.clock.Now())
291292
292- require.Equal(t, ports.ParkedRouteRejected, status)
293+ require.Equal(t, protocol.ParkedRouteRejected, status)
293294 ac.parkedRouteMu.Lock()
294295 require.NotNil(t, ac.parkedRoute)
295296 ac.parkedRouteMu.Unlock()
@@ -328,10 +329,10 @@ func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
328329 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {
329330 d, source, ac, sends, releases := newManualTabSession(t, 1)
330331 defer releaseAll(releases)
331- leaseID := ports.ParkedRouteLeaseID{1}
332- for _, request := range []ports.ParkedRouteRequest{
333- {RequestID: 1, LeaseID: leaseID, Action: ports.ParkedRouteSwitch},
334- {RequestID: 2, Action: ports.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}},
332+ leaseID := protocol.ParkedRouteLeaseID{1}
333+ for _, request := range []protocol.ParkedRouteRequest{
334+ {RequestID: 1, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch},
335+ {RequestID: 2, Action: protocol.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}},
335336 {RequestID: 3, LeaseID: leaseID, Action: 99},
336337 } {
337338 capability := source.captureAttachmentCapability(ac, ac.transport())
@@ -358,7 +359,7 @@ func TestParkedRouteResponseFailureDoesNotDrainItsOwnFrameEffect(t *testing.T) {
358359
359360 result := make(chan bool, 1)
360361 go func() {
361- result <- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})
362+ result <- d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})
362363 }()
363364 require.False(t, awaitTestValue(t, result, "parked-route send failure cleanup"))
364365 d.attachmentCleanupWg.Wait()
@@ -367,7 +368,7 @@ func TestParkedRouteResponseFailureDoesNotDrainItsOwnFrameEffect(t *testing.T) {
367368 func TestParkedRouteDetachClearsLeaseAndSuspension(t *testing.T) {
368369 d, source, ac, sends, releases := newManualTabSession(t, 1)
369370 defer releaseAll(releases)
370- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
371+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
371372
372373 _ = armAndPrepareParkedRoute(t, d, source, ac, sends)
373374 require.True(t, ac.parkedRouteOutput.Load())
M internal/usecase/daemon/picker.go +7 -6
@@ -10,6 +10,7 @@ import (
1010 renderer "github.com/bnema/vev-vt"
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
1314 "github.com/bnema/vev/internal/usecase/layout"
1415 "github.com/bnema/vev/internal/usecase/picker"
1516 "github.com/bnema/vev/internal/usecase/ui"
@@ -377,14 +378,14 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...*
377378 committing := (result.action == '\r' || result.action == '\n') && ok
378379 backNavigationSent := false
379380 if result.exit && !committing {
380- back := ac.startupOverlay == ports.StartupOverlaySessionPicker && ac.navigationCapabilities&ports.NavigationCapabilityBack != 0
381+ back := ac.startupOverlay == protocol.StartupOverlaySessionPicker && ac.navigationCapabilities&protocol.NavigationCapabilityBack != 0
381382 if back {
382383 if len(effects) == 0 || effects[0] == nil {
383384 d.invalidateRender(sess, ac, true, "picker.go")
384385 return
385386 }
386387 effect := effects[0]
387- if err := d.sendNavigationActionForAttachment(effect, ports.NavigationBack); err != nil {
388+ if err := d.sendNavigationActionForAttachment(effect, protocol.NavigationBack); err != nil {
388389 d.reportAttachmentError(sess, err)
389390 d.invalidateRender(sess, ac, true, "picker.go")
390391 return
@@ -599,7 +600,7 @@ func remotePickerPreviewSize(size domain.Size) (uint16, uint16) {
599600 if width <= 0 || height <= 0 {
600601 return 0, 0
601602 }
602- return uint16(min(width, ports.RemotePreviewMaxWidth)), uint16(min(height, ports.RemotePreviewMaxHeight))
603+ return uint16(min(width, protocol.RemotePreviewMaxWidth)), uint16(min(height, protocol.RemotePreviewMaxHeight))
603604 }
604605
605606 func staticRemotePickerPreview(width, height uint16, message string) picker.Preview {
@@ -887,7 +888,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
887888 d.mu.Lock()
888889 var targetSess *session
889890 var sessionName string
890- var exactTarget *ports.ExactSessionTarget
891+ var exactTarget *protocol.ExactSessionTarget
891892 var matches bool
892893 if target.Name != "" {
893894 var stopped inactiveSession
@@ -895,7 +896,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
895896 targetSess, stopped, stoppedTarget, matches = d.resolveNamedLifecycleTargetLocked(target)
896897 if stoppedTarget && matches {
897898 sessionName = stopped.name
898- exactTarget = &ports.ExactSessionTarget{LifecycleID: stopped.incarnation, SessionName: sessionName}
899+ exactTarget = &protocol.ExactSessionTarget{LifecycleID: stopped.incarnation, SessionName: sessionName}
899900 }
900901 } else {
901902 targetSess = d.sessions[target.Session]
@@ -904,7 +905,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
904905 if matches && targetSess != nil {
905906 targetSess.mu.Lock()
906907 sessionName = targetSess.name
907- exactTarget = &ports.ExactSessionTarget{LifecycleID: targetSess.incarnation, SessionName: sessionName}
908+ exactTarget = &protocol.ExactSessionTarget{LifecycleID: targetSess.incarnation, SessionName: sessionName}
908909 targetSess.mu.Unlock()
909910 }
910911 d.mu.Unlock()
M internal/usecase/daemon/picker_test.go +5 -4
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/vev/internal/persist"
1717 "github.com/bnema/vev/internal/ports"
1818 portsmocks "github.com/bnema/vev/internal/ports/mocks"
19+ "github.com/bnema/vev/internal/protocol"
1920 "github.com/bnema/vev/internal/usecase/layout"
2021 "github.com/bnema/vev/internal/usecase/picker"
2122 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
@@ -892,8 +893,8 @@ func TestPickerNavigationBackSendFailureKeepsPickerOpen(t *testing.T) {
892893 const sendErr = "navigation back send failed"
893894 tr := &remotePickerSendErrorTransport{err: errors.New(sendErr)}
894895 ac.replaceTransport(tr)
895- ac.startupOverlay = ports.StartupOverlaySessionPicker
896- ac.navigationCapabilities = ports.NavigationCapabilityBack
896+ ac.startupOverlay = protocol.StartupOverlaySessionPicker
897+ ac.navigationCapabilities = protocol.NavigationCapabilityBack
897898 ac.overlays.pickerMu.Lock()
898899 ac.overlays.picker = picker.New(nil, picker.SelectionConfig{})
899900 ac.overlays.pickerGeneration++
@@ -926,8 +927,8 @@ func TestPickerNavigationBackWithoutCallerTicketKeepsPickerOpen(t *testing.T) {
926927 t.Run(tc.name, func(t *testing.T) {
927928 d, sess, ac, _, releases := newManualTabSession(t, 1)
928929 defer releases[0]()
929- ac.startupOverlay = ports.StartupOverlaySessionPicker
930- ac.navigationCapabilities = ports.NavigationCapabilityBack
930+ ac.startupOverlay = protocol.StartupOverlaySessionPicker
931+ ac.navigationCapabilities = protocol.NavigationCapabilityBack
931932 ac.overlays.pickerMu.Lock()
932933 ac.overlays.picker = picker.New([]picker.SessionView{{ID: sess.id, Name: sess.name, Tabs: []picker.TabEntry{{Name: "tab"}}, Active: 0}}, picker.SelectionConfig{})
933934 ac.overlays.pickerGeneration++
M internal/usecase/daemon/recent_route_presentation_test.go +5 -5
@@ -3,7 +3,7 @@ package daemon
33 import (
44 "testing"
55
6- "github.com/bnema/vev/internal/ports"
6+ "github.com/bnema/vev/internal/protocol"
77 "github.com/bnema/vev/internal/usecase/command"
88 "github.com/bnema/vev/internal/usecase/palette"
99 "github.com/stretchr/testify/require"
@@ -76,11 +76,11 @@ func TestFormatRecentRoutePresentations(t *testing.T) {
7676 }
7777
7878 func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T) {
79- snapshot := ports.RecentRouteSnapshot{
79+ snapshot := protocol.RecentRouteSnapshot{
8080 Generation: 1,
81- Entries: []ports.RecentRouteEntry{
82- {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: ports.RouteKindLocal, Attention: true},
83- testRouteEntry(2, 1, "logs", 2, ports.RouteKindLocal),
81+ Entries: []protocol.RecentRouteEntry{
82+ {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true},
83+ testRouteEntry(2, 1, "logs", 2, protocol.RouteKindLocal),
8484 },
8585 }
8686
M internal/usecase/daemon/recent_route_snapshot.go +10 -10
@@ -2,26 +2,26 @@ package daemon
22
33 import (
44 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
66 )
77
8-func activeRouteEntryForLifecycle(snapshot ports.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) (ports.RecentRouteEntry, bool) {
8+func activeRouteEntryForLifecycle(snapshot protocol.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) (protocol.RecentRouteEntry, bool) {
99 active := snapshot.ActiveEntry
10- activeRef := ports.RouteRef{Key: active.Key, Generation: active.Generation}
10+ activeRef := protocol.RouteRef{Key: active.Key, Generation: active.Generation}
1111 if lifecycle == (domain.SessionLifecycleID{}) || active.Target.LifecycleID != lifecycle || snapshot.Active != activeRef {
12- return ports.RecentRouteEntry{}, false
12+ return protocol.RecentRouteEntry{}, false
1313 }
1414 return active, true
1515 }
1616
17-func recentRoutePresentationsFromSnapshot(snapshot ports.RecentRouteSnapshot) []recentRoutePresentation {
17+func recentRoutePresentationsFromSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRoutePresentation {
1818 if snapshot.Entries == nil {
1919 return nil
2020 }
2121 out := make([]recentRoutePresentation, len(snapshot.Entries))
2222 for i, entry := range snapshot.Entries {
2323 kind := recentRouteLocal
24- if entry.Kind == ports.RouteKindRemote {
24+ if entry.Kind == protocol.RouteKindRemote {
2525 kind = recentRouteRemote
2626 }
2727 out[i] = recentRoutePresentation{
@@ -35,20 +35,20 @@ func recentRoutePresentationsFromSnapshot(snapshot ports.RecentRouteSnapshot) []
3535 return out
3636 }
3737
38-func formatRecentRouteSnapshot(snapshot ports.RecentRouteSnapshot) []recentRouteDisplay {
38+func formatRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
3939 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))
4040 }
4141
4242 // formatRecentRouteSnapshotForAttachment resolves attention against this
4343 // daemon's live sessions. The client retains route ordering and presentation;
4444 // it subscribes only the exact routes this daemon serves.
45-func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snapshot ports.RecentRouteSnapshot) []recentRouteDisplay {
45+func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
4646 presentations := recentRoutePresentationsFromSnapshot(snapshot)
4747 if d == nil || ac == nil {
4848 return formatRecentRoutePresentations(presentations)
4949 }
5050 for i, entry := range snapshot.Entries {
51- target, ok := ac.routeAttentionTarget(ports.RouteRef{Key: entry.Key, Generation: entry.Generation})
51+ target, ok := ac.routeAttentionTarget(protocol.RouteRef{Key: entry.Key, Generation: entry.Generation})
5252 if ok {
5353 presentations[i].attention = d.routeHasAttention(target)
5454 }
@@ -56,7 +56,7 @@ func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snap
5656 return formatRecentRoutePresentations(presentations)
5757 }
5858
59-func (d *Daemon) routeHasAttention(target ports.ExactSessionTarget) bool {
59+func (d *Daemon) routeHasAttention(target protocol.ExactSessionTarget) bool {
6060 d.mu.Lock()
6161 sess := d.findByNameLocked(target.SessionName)
6262 d.mu.Unlock()
M internal/usecase/daemon/recent_route_snapshot_test.go +25 -24
@@ -5,21 +5,22 @@ import (
55
66 "github.com/bnema/vev/internal/domain"
77 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
89 "github.com/stretchr/testify/require"
910 )
1011
1112 func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {
1213 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
13- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
14+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
1415 Generation: 2,
15- Active: ports.RouteRef{Key: 3, Generation: 2},
16- Entries: []ports.RecentRouteEntry{
17- {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: ports.RouteKindRemote},
18- testRouteEntry(1, 1, "work", 1, ports.RouteKindLocal),
16+ Active: protocol.RouteRef{Key: 3, Generation: 2},
17+ Entries: []protocol.RecentRouteEntry{
18+ {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote},
19+ testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),
1920 },
2021 })
2122
22- state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, ports.RecentRouteSnapshot{})
23+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
2324
2425 require.Len(t, state.mru, 2)
2526 require.Equal(t, []string{"logs@edge", "work"}, []string{state.mru[0].name, state.mru[1].name})
@@ -40,21 +41,21 @@ func TestAttachmentStatusResolvesSubscribedRouteAttention(t *testing.T) {
4041 sess.incarnation = domain.SessionLifecycleID{9}
4142 sess.tabs[0].attention = true
4243 sess.mu.Unlock()
43- ref := ports.RouteRef{Key: 2, Generation: 1}
44- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
44+ ref := protocol.RouteRef{Key: 2, Generation: 1}
45+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
4546 Generation: 2,
46- Active: ports.RouteRef{Key: 3, Generation: 2},
47- Entries: []ports.RecentRouteEntry{{Key: ref.Key, Generation: ref.Generation, Target: ports.ExactSessionTarget{LifecycleID: tt.targetID, SessionName: sess.name}, Name: sess.name, Kind: ports.RouteKindLocal}},
47+ Active: protocol.RouteRef{Key: 3, Generation: 2},
48+ Entries: []protocol.RecentRouteEntry{{Key: ref.Key, Generation: ref.Generation, Target: protocol.ExactSessionTarget{LifecycleID: tt.targetID, SessionName: sess.name}, Name: sess.name, Kind: protocol.RouteKindLocal}},
4849 })
49- ac.setRouteAttentionSubscription(ports.RouteAttentionSubscription{Targets: []ports.RouteAttentionTarget{{
50+ ac.setRouteAttentionSubscription(protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{{
5051 Ref: ref,
51- Target: ports.ExactSessionTarget{
52+ Target: protocol.ExactSessionTarget{
5253 LifecycleID: tt.targetID,
5354 SessionName: sess.name,
5455 },
5556 }}})
5657
57- state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, ports.RecentRouteSnapshot{})
58+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
5859
5960 require.Len(t, state.mru, 1)
6061 require.Equal(t, tt.attention, state.mru[0].attention)
@@ -66,9 +67,9 @@ func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) {
6667 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
6768 token := sess.captureAttachmentCapability(ac, ac.transport())
6869 ac.installTestAttachmentCapability(token)
69- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
70+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
7071
71- payload, err := ports.MarshalRecentRouteSnapshot(ports.RecentRouteSnapshot{Generation: 2})
72+ payload, err := ports.MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 2})
7273 require.NoError(t, err)
7374 require.False(t, d.handleAttachmentClientFrame(token, ports.Frame{Type: ports.MsgRecentRouteSnapshot, Payload: payload}))
7475
@@ -77,8 +78,8 @@ func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) {
7778
7879 func TestAttachmentRouteSnapshotCopiesPublishedEntries(t *testing.T) {
7980 _, _, ac, _ := newManualSessionWithPTYs(t, nil)
80- entries := []ports.RecentRouteEntry{testRouteEntry(1, 2, "before", 1, ports.RouteKindLocal)}
81- snapshot := ports.RecentRouteSnapshot{Generation: 3, Entries: entries}
81+ entries := []protocol.RecentRouteEntry{testRouteEntry(1, 2, "before", 1, protocol.RouteKindLocal)}
82+ snapshot := protocol.RecentRouteSnapshot{Generation: 3, Entries: entries}
8283 ac.setRouteSnapshot(snapshot)
8384
8485 entries[0].Name = "after"
@@ -102,16 +103,16 @@ func TestPaletteRecentRouteSelectionSendsTypedClientAction(t *testing.T) {
102103
103104 exec := paletteExec{
104105 d: d, sess: source, attachment: source, ac: ac,
105- routeSnapshot: ports.RecentRouteSnapshot{
106+ routeSnapshot: protocol.RecentRouteSnapshot{
106107 Generation: 4,
107- Entries: []ports.RecentRouteEntry{testRouteEntry(2, 3, "remote", 2, ports.RouteKindRemote)},
108+ Entries: []protocol.RecentRouteEntry{testRouteEntry(2, 3, "remote", 2, protocol.RouteKindRemote)},
108109 },
109110 effect: effect,
110111 }
111112 require.NoError(t, exec.JumpRecentSession(1))
112113
113114 frames := transport.Sends()
114- var action ports.RouteNavigationAction
115+ var action protocol.RouteNavigationAction
115116 found := false
116117 for _, frame := range frames {
117118 if frame.Type != ports.MsgNavigateRecentRoute {
@@ -123,14 +124,14 @@ func TestPaletteRecentRouteSelectionSendsTypedClientAction(t *testing.T) {
123124 found = true
124125 }
125126 require.True(t, found)
126- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 3}, action)
127+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 3}, action)
127128 }
128129
129130 func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
130- snapshot := ports.RecentRouteSnapshot{
131+ snapshot := protocol.RecentRouteSnapshot{
131132 Generation: 8,
132- Entries: []ports.RecentRouteEntry{{
133- Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: ports.RouteKindRemote,
133+ Entries: []protocol.RecentRouteEntry{{
134+ Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,
134135 }},
135136 }
136137
M internal/usecase/daemon/remote_attach.go +12 -11
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.
@@ -27,8 +28,8 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
2728 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires attach or resume"}
2829 }
2930 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
30- var protocol *protoErr
31- if errors.As(err, &protocol) && protocol.code == ports.ErrInternal {
31+ var protocolErr *protoErr
32+ if errors.As(err, &protocolErr) && protocolErr.code == ports.ErrInternal {
3233 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}
3334 }
3435 return nil, nil, remoteTargetError(err)
@@ -102,10 +103,10 @@ func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTa
102103 return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames)
103104 }
104105
105-func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, action ports.NavigationAction) error {
106- directive := ports.NavigationDirective{Action: action}
106+func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, action protocol.NavigationAction) error {
107+ directive := protocol.NavigationDirective{Action: action}
107108 armed := false
108- if action == ports.NavigationOpenHomePicker {
109+ if action == protocol.NavigationOpenHomePicker {
109110 leaseID, err := d.armParkedRoute(effect)
110111 if err != nil {
111112 return err
@@ -130,7 +131,7 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
130131 return nil
131132 }
132133
133-func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action ports.RouteNavigationAction) error {
134+func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action protocol.RouteNavigationAction) error {
134135 payload, err := ports.MarshalRouteNavigationAction(action)
135136 if err != nil {
136137 return errAttachmentTransition
@@ -143,8 +144,8 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
143144 return errAttachmentTransition
144145 }
145146 effect.sess.mu.Lock()
146- identity := ports.CommittedRouteIdentity{
147- Target: ports.ExactSessionTarget{LifecycleID: effect.sess.incarnation, SessionName: effect.sess.name},
147+ identity := protocol.CommittedRouteIdentity{
148+ Target: protocol.ExactSessionTarget{LifecycleID: effect.sess.incarnation, SessionName: effect.sess.name},
148149 Ephemeral: effect.sess.ephemeral,
149150 }
150151 effect.sess.mu.Unlock()
@@ -188,9 +189,9 @@ func (d *Daemon) remoteTargetMatchesSessionLocked(sess *session, target domain.R
188189 }
189190
190191 func remoteTargetError(err error) error {
191- var protocol *protoErr
192- if errors.As(err, &protocol) {
193- return protocol
192+ var protocolErr *protoErr
193+ if errors.As(err, &protocolErr) {
194+ return protocolErr
194195 }
195196 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
196197 return err
M internal/usecase/daemon/remote_attach_test.go +3 -2
@@ -8,6 +8,7 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 "github.com/bnema/vev/internal/usecase/picker"
1213 )
1314
@@ -34,7 +35,7 @@ func TestLocalPickerOfferCarriesExactLifecycle(t *testing.T) {
3435 require.Empty(t, got.Endpoint)
3536 require.Equal(t, ports.IntentAttach, got.Intent)
3637 require.True(t, got.SamePeer)
37- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
38+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
3839 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client confirms the switch")
3940 }
4041
@@ -66,7 +67,7 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) {
6667 target, err := ports.UnmarshalAttachTarget(handoff.Payload)
6768 require.NoError(t, err)
6869 require.False(t, target.SamePeer)
69- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "stopped"}, target.ExactTarget)
70+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "stopped"}, target.ExactTarget)
7071 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client receives the handoff and closes")
7172 require.True(t, attachmentRegistered(source, ac))
7273 }
M internal/usecase/daemon/remote_picker_test.go +7 -6
@@ -17,6 +17,7 @@ import (
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
1919 portsmocks "github.com/bnema/vev/internal/ports/mocks"
20+ "github.com/bnema/vev/internal/protocol"
2021 "github.com/bnema/vev/internal/usecase/picker"
2122 )
2223
@@ -842,18 +843,18 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing
842843 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
843844 tests := []struct {
844845 name string
845- action ports.NavigationAction
846+ action protocol.NavigationAction
846847 }{
847- {name: "home picker", action: ports.NavigationOpenHomePicker},
848- {name: "back", action: ports.NavigationBack},
848+ {name: "home picker", action: protocol.NavigationOpenHomePicker},
849+ {name: "back", action: protocol.NavigationBack},
849850 }
850851
851852 for _, tt := range tests {
852853 t.Run(tt.name, func(t *testing.T) {
853854 d := newRemotePickerDaemon(nil)
854855 sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")
855- if tt.action == ports.NavigationOpenHomePicker {
856- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
856+ if tt.action == protocol.NavigationOpenHomePicker {
857+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
857858 }
858859 token := sess.captureAttachmentCapability(ac, ac.transport())
859860 effect, admitted := ac.beginAttachmentEffect(token)
@@ -865,7 +866,7 @@ func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
865866 directive, err := ports.UnmarshalNavigationDirective(frame.Payload)
866867 require.NoError(t, err)
867868 require.Equal(t, tt.action, directive.Action)
868- if tt.action == ports.NavigationOpenHomePicker {
869+ if tt.action == protocol.NavigationOpenHomePicker {
869870 require.False(t, directive.LeaseID.IsZero())
870871 } else {
871872 require.True(t, directive.LeaseID.IsZero())
M internal/usecase/daemon/remote_preview.go +20 -19
@@ -6,71 +6,72 @@ import (
66 renderer "github.com/bnema/vev-vt"
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 )
1011
1112 func (d *Daemon) handleRemotePreview(tr ports.Transport, frame ports.Frame) error {
1213 defer func() { _ = tr.Close() }()
1314 request, err := ports.UnmarshalRemotePreviewRequest(frame.Payload)
1415 if err != nil {
15- return d.sendRemotePreview(tr, ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewMalformed})
16+ return d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
1617 }
1718 preview, err := d.captureRemotePreview(request)
1819 if err != nil {
19- status := ports.RemotePreviewUnavailable
20+ status := protocol.RemotePreviewUnavailable
2021 if errors.Is(err, errRemotePreviewNoSuchTarget) {
21- status = ports.RemotePreviewNoSuchTarget
22+ status = protocol.RemotePreviewNoSuchTarget
2223 }
23- preview = ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: status}
24+ preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: status}
2425 }
2526 return d.sendRemotePreview(tr, preview)
2627 }
2728
28-func (d *Daemon) sendRemotePreview(tr ports.Transport, preview ports.RemotePreview) error {
29+func (d *Daemon) sendRemotePreview(tr ports.Transport, preview protocol.RemotePreview) error {
2930 payload := ports.MarshalRemotePreview(preview)
3031 if payload == nil {
31- payload = ports.MarshalRemotePreview(ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewMalformed})
32+ payload = ports.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
3233 }
3334 return d.boundedControlSend(tr, ports.Frame{Type: ports.MsgRemotePreviewResponse, Payload: payload})
3435 }
3536
3637 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
3738
38-func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports.RemotePreview, error) {
39+func (d *Daemon) captureRemotePreview(request protocol.RemotePreviewRequest) (protocol.RemotePreview, error) {
3940 target := request.Target
4041 d.mu.Lock()
4142 sess := d.findByNameLocked(target.SessionName)
4243 if sess == nil || sess.incarnation != target.LifecycleID {
4344 d.mu.Unlock()
44- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
45+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
4546 }
4647 index, ok := remoteTargetTabIndexLocked(sess, target)
4748 d.mu.Unlock()
4849 if !ok {
49- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
50+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
5051 }
5152
5253 sess.mu.Lock()
5354 if index < 0 || index >= len(sess.tabs) {
5455 sess.mu.Unlock()
55- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
56+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
5657 }
5758 tab := sess.tabs[index]
5859 if tab == nil || domain.TabStableID(tab.stableID) != target.LiveTabID {
5960 sess.mu.Unlock()
60- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
61+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
6162 }
6263 sess.mu.Unlock()
6364 tab.mu.Lock()
6465 pane := tab.focusedPane()
6566 if pane == nil {
6667 tab.mu.Unlock()
67- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
68+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
6869 }
6970 pane.mu.Lock()
7071 if pane.screen == nil {
7172 pane.mu.Unlock()
7273 tab.mu.Unlock()
73- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
74+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
7475 }
7576 source := pane.screen.Frame.Clone()
7677 revision := pane.syncGen
@@ -83,7 +84,7 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
8384 width := min(int(request.Width), source.Width)
8485 height := min(int(request.Height), source.Height)
8586 if width <= 0 || height <= 0 {
86- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget
87+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
8788 }
8889 startY, endY := source.Height-height, source.Height
8990 if last := remotePreviewLastContentRow(source, width, startY, endY); last >= startY {
@@ -101,9 +102,9 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
101102 cells = append(cells, cell)
102103 }
103104 }
104- preview := ports.RemotePreview{
105- Version: ports.RemotePreviewSchemaVersion,
106- Status: ports.RemotePreviewOK,
105+ preview := protocol.RemotePreview{
106+ Version: protocol.RemotePreviewSchemaVersion,
107+ Status: protocol.RemotePreviewOK,
107108 LifecycleID: target.LifecycleID,
108109 TabID: target.LiveTabID,
109110 Revision: revision,
@@ -111,10 +112,10 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
111112 Height: uint16(height),
112113 Cells: cells,
113114 }
114- if err := ports.ValidateRemotePreview(preview); err != nil {
115+ if err := protocol.ValidateRemotePreview(preview); err != nil {
115116 // A crop must not split a wide cell. Return a safe unavailable response
116117 // instead of exposing a malformed cell stream.
117- return ports.RemotePreview{}, err
118+ return protocol.RemotePreview{}, err
118119 }
119120 return preview, nil
120121 }
M internal/usecase/daemon/remote_preview_cache.go +22 -22
@@ -9,7 +9,7 @@ import (
99 "time"
1010
1111 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1313 )
1414
1515 const (
@@ -34,14 +34,14 @@ type remotePreviewCacheKey struct {
3434 }
3535
3636 type remotePreviewCacheEntry struct {
37- Preview ports.RemotePreview
37+ Preview protocol.RemotePreview
3838 Fetched time.Time
3939 Used time.Time
4040 }
4141
4242 type remotePreviewFlight struct {
4343 done chan struct{}
44- preview ports.RemotePreview
44+ preview protocol.RemotePreview
4545 err error
4646 }
4747
@@ -53,7 +53,7 @@ type remotePreviewState struct {
5353 slots chan struct{}
5454 }
5555
56-func cloneRemotePreview(preview ports.RemotePreview) ports.RemotePreview {
56+func cloneRemotePreview(preview protocol.RemotePreview) protocol.RemotePreview {
5757 preview.Cells = slices.Clone(preview.Cells)
5858 return preview
5959 }
@@ -82,24 +82,24 @@ func remotePreviewKeyFor(target domain.RemoteSessionTarget, width, height uint16
8282 }
8383 }
8484
85-func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
85+func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
8686 if d == nil || d.remotePreviewClient == nil {
87- return ports.RemotePreview{}, errors.New("remote preview client unavailable")
87+ return protocol.RemotePreview{}, errors.New("remote preview client unavailable")
8888 }
8989 if ctx == nil {
9090 ctx = context.Background()
9191 }
9292 if err := ctx.Err(); err != nil {
93- return ports.RemotePreview{}, err
93+ return protocol.RemotePreview{}, err
9494 }
95- request := ports.RemotePreviewRequest{
96- Version: ports.RemotePreviewSchemaVersion,
95+ request := protocol.RemotePreviewRequest{
96+ Version: protocol.RemotePreviewSchemaVersion,
9797 Target: target,
9898 Width: width,
9999 Height: height,
100100 }
101- if err := ports.ValidateRemotePreviewRequest(request); err != nil {
102- return ports.RemotePreview{}, err
101+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
102+ return protocol.RemotePreview{}, err
103103 }
104104
105105 key := remotePreviewKeyFor(target, width, height)
@@ -139,7 +139,7 @@ func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSes
139139
140140 if until, cooling := d.remotePreview.cooldowns[key]; cooling && now.Before(until) {
141141 d.remotePreview.mu.Unlock()
142- return ports.RemotePreview{}, errRemotePreviewCooldown
142+ return protocol.RemotePreview{}, errRemotePreviewCooldown
143143 }
144144 if flight := d.remotePreview.flights[key]; flight != nil {
145145 d.remotePreview.mu.Unlock()
@@ -160,12 +160,12 @@ func (d *Daemon) remotePreviewContext() context.Context {
160160 return context.Background()
161161 }
162162
163-func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (ports.RemotePreview, error) {
163+func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (protocol.RemotePreview, error) {
164164 select {
165165 case <-flight.done:
166166 return cloneRemotePreview(flight.preview), flight.err
167167 case <-ctx.Done():
168- return ports.RemotePreview{}, ctx.Err()
168+ return protocol.RemotePreview{}, ctx.Err()
169169 }
170170 }
171171
@@ -173,7 +173,7 @@ func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (
173173 // while still publishing the refreshed result once the same-key flight finishes.
174174 // A completed flight is read from the cache without starting a second remote
175175 // request.
176-func (d *Daemon) awaitRemotePreviewRefresh(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
176+func (d *Daemon) awaitRemotePreviewRefresh(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
177177 key := remotePreviewKeyFor(target, width, height)
178178 d.remotePreview.mu.Lock()
179179 flight := d.remotePreview.flights[key]
@@ -188,20 +188,20 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
188188 select {
189189 case d.remotePreview.slots <- struct{}{}:
190190 case <-ctx.Done():
191- d.finishRemotePreviewFlight(key, flight, ports.RemotePreview{}, ctx.Err())
191+ d.finishRemotePreviewFlight(key, flight, protocol.RemotePreview{}, ctx.Err())
192192 return
193193 }
194- var preview ports.RemotePreview
194+ var preview protocol.RemotePreview
195195 var err error
196196 defer func() {
197197 if recovered := recover(); recovered != nil {
198198 err = fmt.Errorf("remote preview flight panicked: %v", recovered)
199- preview = ports.RemotePreview{}
199+ preview = protocol.RemotePreview{}
200200 d.finishRemotePreviewFlight(key, flight, preview, err)
201201 panic(recovered)
202202 }
203203 if err != nil {
204- preview = ports.RemotePreview{}
204+ preview = protocol.RemotePreview{}
205205 }
206206 d.finishRemotePreviewFlight(key, flight, preview, err)
207207 }()
@@ -209,9 +209,9 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
209209
210210 preview, err = d.remotePreviewClient.Preview(ctx, target, width, height)
211211 if err == nil {
212- if validationErr := ports.ValidateRemotePreview(preview); validationErr != nil {
212+ if validationErr := protocol.ValidateRemotePreview(preview); validationErr != nil {
213213 err = validationErr
214- } else if preview.Status != ports.RemotePreviewOK {
214+ } else if preview.Status != protocol.RemotePreviewOK {
215215 err = fmt.Errorf("remote preview returned status %d", preview.Status)
216216 } else if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
217217 err = errors.New("remote preview identity changed")
@@ -219,7 +219,7 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
219219 }
220220 }
221221
222-func (d *Daemon) finishRemotePreviewFlight(key remotePreviewCacheKey, flight *remotePreviewFlight, preview ports.RemotePreview, err error) {
222+func (d *Daemon) finishRemotePreviewFlight(key remotePreviewCacheKey, flight *remotePreviewFlight, preview protocol.RemotePreview, err error) {
223223 now := d.clock.Now()
224224 d.remotePreview.mu.Lock()
225225 for cooldownKey, until := range d.remotePreview.cooldowns {
M internal/usecase/daemon/remote_preview_cache_test.go +14 -13
@@ -12,6 +12,7 @@ import (
1212 renderer "github.com/bnema/vev-vt"
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 "github.com/bnema/vev/internal/usecase/picker"
1617 )
1718
@@ -37,7 +38,7 @@ func (c *remotePreviewTestClock) Advance(delta time.Duration) {
3738 type remotePreviewTestClient struct {
3839 mu sync.Mutex
3940 calls int
40- result ports.RemotePreview
41+ result protocol.RemotePreview
4142 err error
4243 started chan struct{}
4344 finished chan struct{}
@@ -50,7 +51,7 @@ type remotePreviewTestClient struct {
5051 finishedSignal bool
5152 }
5253
53-func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
54+func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
5455 c.mu.Lock()
5556 c.calls++
5657 c.lastTarget = target
@@ -69,7 +70,7 @@ func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.Rem
6970 select {
7071 case <-release:
7172 case <-ctx.Done():
72- return ports.RemotePreview{}, ctx.Err()
73+ return protocol.RemotePreview{}, ctx.Err()
7374 }
7475 }
7576 }
@@ -111,13 +112,13 @@ func remotePreviewCacheTarget() domain.RemoteSessionTarget {
111112 }
112113 }
113114
114-func remotePreviewCacheResult(target domain.RemoteSessionTarget, revision uint64) ports.RemotePreview {
115+func remotePreviewCacheResult(target domain.RemoteSessionTarget, revision uint64) protocol.RemotePreview {
115116 return remotePreviewCacheResultRune(target, revision, 'x')
116117 }
117118
118-func remotePreviewCacheResultRune(target domain.RemoteSessionTarget, revision uint64, value rune) ports.RemotePreview {
119- return ports.RemotePreview{
120- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,
119+func remotePreviewCacheResultRune(target domain.RemoteSessionTarget, revision uint64, value rune) protocol.RemotePreview {
120+ return protocol.RemotePreview{
121+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
121122 LifecycleID: target.LifecycleID, TabID: target.LiveTabID,
122123 Revision: revision, Width: 1, Height: 1,
123124 Cells: []renderer.Cell{{Rune: value, Style: renderer.DefaultStyle()}},
@@ -136,7 +137,7 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
136137 d.remotePreviewClient = client
137138
138139 firstDone := make(chan struct{})
139- var first ports.RemotePreview
140+ var first protocol.RemotePreview
140141 var firstErr error
141142 go func() {
142143 first, firstErr = d.fetchRemotePreview(context.Background(), target, 1, 1)
@@ -145,7 +146,7 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
145146 <-client.started
146147
147148 secondDone := make(chan struct{})
148- var second ports.RemotePreview
149+ var second protocol.RemotePreview
149150 var secondErr error
150151 go func() {
151152 second, secondErr = d.fetchRemotePreview(context.Background(), target, 1, 1)
@@ -169,12 +170,12 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
169170 func TestFetchRemotePreviewAdapterTimeoutAppliesCooldown(t *testing.T) {
170171 clock := &remotePreviewTestClock{now: time.Unix(250, 0)}
171172 target := remotePreviewCacheTarget()
172- client := &remotePreviewTestClient{err: ports.ErrRemotePreviewTimeout}
173+ client := &remotePreviewTestClient{err: protocol.ErrRemotePreviewTimeout}
173174 d := newTestDaemon(t, nil, clock)
174175 d.remotePreviewClient = client
175176
176177 _, firstErr := d.fetchRemotePreview(context.Background(), target, 1, 1)
177- require.ErrorIs(t, firstErr, ports.ErrRemotePreviewTimeout)
178+ require.ErrorIs(t, firstErr, protocol.ErrRemotePreviewTimeout)
178179 _, secondErr := d.fetchRemotePreview(context.Background(), target, 1, 1)
179180 require.ErrorIs(t, secondErr, errRemotePreviewCooldown)
180181 require.Equal(t, 1, client.Calls())
@@ -482,7 +483,7 @@ func TestFetchRemotePreviewRejectsInvalidDimensionsBeforeRemoteIO(t *testing.T)
482483 d := newTestDaemon(t, nil, clock)
483484 d.remotePreviewClient = client
484485
485- _, err := d.fetchRemotePreview(context.Background(), remotePreviewCacheTarget(), ports.RemotePreviewMaxWidth+1, 1)
486- require.ErrorIs(t, err, ports.ErrInvalidRemotePreviewRequest)
486+ _, err := d.fetchRemotePreview(context.Background(), remotePreviewCacheTarget(), protocol.RemotePreviewMaxWidth+1, 1)
487+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
487488 require.Zero(t, client.Calls())
488489 }
M internal/usecase/daemon/remote_preview_capture_test.go +8 -8
@@ -8,7 +8,7 @@ import (
88 vt "github.com/bnema/vev-vt"
99 renderer "github.com/bnema/vev-vt/ansi"
1010 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1212 )
1313
1414 func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {
@@ -28,8 +28,8 @@ func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {
2828 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
2929 SessionName: "work", LiveTabID: "tab-1",
3030 }
31- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
32- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 2,
31+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
32+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 2,
3333 })
3434 require.NoError(t, err)
3535 require.Equal(t, []rune{'b', 'c'}, []rune{preview.Cells[0].Rune, preview.Cells[1].Rune})
@@ -112,8 +112,8 @@ func TestCaptureRemotePreviewCompactsBlankTail(t *testing.T) {
112112 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
113113 SessionName: "work", LiveTabID: "tab-1",
114114 }
115- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
116- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: test.width, Height: test.height,
115+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
116+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: test.width, Height: test.height,
117117 })
118118 require.NoError(t, err)
119119 require.Equal(t, test.wantHeight, preview.Height)
@@ -156,13 +156,13 @@ func TestCaptureRemotePreviewDoesNotSplitWideRuneAtCropBoundary(t *testing.T) {
156156 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
157157 SessionName: "work", LiveTabID: "tab-1",
158158 }
159- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
160- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 2, Height: 1,
159+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
160+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 2, Height: 1,
161161 })
162162 require.NoError(t, err)
163163 require.Equal(t, uint16(2), preview.Width)
164164 require.Equal(t, rune(' '), preview.Cells[1].Rune, "a crop cannot expose a dangling wide rune")
165165 require.False(t, preview.Cells[1].Continuation)
166166 require.True(t, preview.Cells[1].Style.Equal(style), "boundary replacement preserves the cell style")
167- require.NoError(t, ports.ValidateRemotePreview(preview))
167+ require.NoError(t, protocol.ValidateRemotePreview(preview))
168168 }
M internal/usecase/daemon/remote_route_test.go +9 -9
@@ -122,9 +122,9 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
122122 // both success and error paths before returning.
123123 d.mu.Lock()
124124 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)
125- var protocol *protoErr
126- require.ErrorAs(t, err, &protocol)
127- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
125+ var protocolErr *protoErr
126+ require.ErrorAs(t, err, &protocolErr)
127+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
128128 d.mu.Lock()
129129 _, retained := d.sessions[sess.id]
130130 d.mu.Unlock()
@@ -146,9 +146,9 @@ func TestFinishRouteAttachPreservesConcurrentAttachment(t *testing.T) {
146146 RemoteTarget: &missing, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
147147 }, true, true)
148148
149- var protocol *protoErr
150- require.ErrorAs(t, err, &protocol)
151- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
149+ var protocolErr *protoErr
150+ require.ErrorAs(t, err, &protocolErr)
151+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
152152 require.Same(t, sess, winner().currentAttachmentSession())
153153 d.mu.Lock()
154154 require.Same(t, sess, d.sessions[sess.id])
@@ -217,7 +217,7 @@ func TestRouteRemoteTargetRejectsSameNameReplacement(t *testing.T) {
217217 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: old, SessionName: "work", LiveTabID: "tab-new"}
218218 hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}
219219 _, _, err := d.routeWithContext(context.Background(), hello, tr)
220- var protocol *protoErr
221- require.ErrorAs(t, err, &protocol)
222- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
220+ var protocolErr *protoErr
221+ require.ErrorAs(t, err, &protocolErr)
222+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
223223 }
M internal/usecase/daemon/remote_stopped_route_test.go +16 -16
@@ -30,9 +30,9 @@ func TestDaemonOwnedNoExactTargetRejectsNewAndEphemeral(t *testing.T) {
3030 Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
3131 }
3232 _, _, err := d.route(hello, &closeTrackingTransport{})
33- var protocol *protoErr
34- require.ErrorAs(t, err, &protocol)
35- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
33+ var protocolErr *protoErr
34+ require.ErrorAs(t, err, &protocolErr)
35+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
3636 d.mu.Lock()
3737 sessions := len(d.sessions)
3838 d.mu.Unlock()
@@ -83,9 +83,9 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
8383 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
8484 }, &closeTrackingTransport{})
8585
86- var protocol *protoErr
87- require.ErrorAs(t, err, &protocol)
88- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
86+ var protocolErr *protoErr
87+ require.ErrorAs(t, err, &protocolErr)
88+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
8989 d.mu.Lock()
9090 defer d.mu.Unlock()
9191 require.Empty(t, d.sessions)
@@ -115,9 +115,9 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
115115 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
116116 }, &closeTrackingTransport{})
117117
118- var protocol *protoErr
119- require.ErrorAs(t, err, &protocol)
120- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
118+ var protocolErr *protoErr
119+ require.ErrorAs(t, err, &protocolErr)
120+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
121121 })
122122 }
123123 }
@@ -138,9 +138,9 @@ func TestRouteRemoteTargetMapsConcurrentInactiveResumeToNoSuchTarget(t *testing.
138138 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
139139 }, &closeTrackingTransport{})
140140
141- var protocol *protoErr
142- require.ErrorAs(t, err, &protocol)
143- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
141+ var protocolErr *protoErr
142+ require.ErrorAs(t, err, &protocolErr)
143+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
144144 }
145145
146146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
@@ -172,8 +172,8 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
172172
173173 err := route(d, newTarget(record))
174174 require.ErrorIs(t, err, cause)
175- var protocol *protoErr
176- require.False(t, errors.As(err, &protocol))
175+ var protocolErr *protoErr
176+ require.False(t, errors.As(err, &protocolErr))
177177 })
178178
179179 t.Run("catalogue read", func(t *testing.T) {
@@ -191,8 +191,8 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
191191
192192 err := route(d, newTarget(record))
193193 require.ErrorIs(t, err, cause)
194- var protocol *protoErr
195- require.False(t, errors.As(err, &protocol))
194+ var protocolErr *protoErr
195+ require.False(t, errors.As(err, &protocolErr))
196196 })
197197 }
198198
M internal/usecase/daemon/render_coordinator_test.go +2 -1
@@ -14,6 +14,7 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/keys"
1819 "github.com/bnema/vev/internal/usecase/layout"
1920 )
@@ -1164,7 +1165,7 @@ func TestProducerInvalidations(t *testing.T) {
11641165 file: "client.go",
11651166 name: "client theme application",
11661167 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
1167- d.applyTheme(sess, ac, ports.Theme{TrueColor: true})
1168+ d.applyTheme(sess, ac, protocol.Theme{TrueColor: true})
11681169 },
11691170 },
11701171 {
M internal/usecase/daemon/render_pipeline.go +3 -2
@@ -6,6 +6,7 @@ import (
66 renderer "github.com/bnema/vev-vt"
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
910 "github.com/bnema/vev/internal/usecase/layout"
1011 "github.com/bnema/vev/internal/usecase/notices"
1112 "github.com/bnema/vev/internal/usecase/palette"
@@ -416,8 +417,8 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
416417 }
417418 entry.core().mu.Lock()
418419 _, owned := entry.core().attachments[ac]
419- position := ports.RoutePosition{
420- Target: ports.ExactSessionTarget{
420+ position := protocol.RoutePosition{
421+ Target: protocol.ExactSessionTarget{
421422 LifecycleID: entry.core().incarnation,
422423 SessionName: entry.core().name,
423424 },
M internal/usecase/daemon/resume_test.go +9 -9
@@ -101,9 +101,9 @@ func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) {
101101 d := newTestDaemon(t, nil, stubClock{})
102102 tr, _ := newCapturingTransport(t)
103103 _, _, err := d.route(helloResumeCapable(ports.IntentResume, "work", 0), tr)
104- var protocol *protoErr
105- require.ErrorAs(t, err, &protocol)
106- require.Equal(t, ports.ErrNoSuchSession, protocol.code)
104+ var protocolErr *protoErr
105+ require.ErrorAs(t, err, &protocolErr)
106+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
107107 d.mu.Lock()
108108 sessionCount := len(d.sessions)
109109 d.mu.Unlock()
@@ -126,9 +126,9 @@ func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T
126126 resume.RemoteTarget = &target
127127 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
128128 _, _, err = d.route(resume, &closeTrackingTransport{})
129- var protocol *protoErr
130- require.ErrorAs(t, err, &protocol)
131- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
129+ var protocolErr *protoErr
130+ require.ErrorAs(t, err, &protocolErr)
131+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
132132 require.Same(t, oldTransport, ac.transport())
133133 d.mu.Lock()
134134 _, parked := d.parked[token]
@@ -155,9 +155,9 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
155155 resume.RemoteTarget = &target
156156 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
157157 _, _, err = d.route(resume, &closeTrackingTransport{})
158- var protocol *protoErr
159- require.ErrorAs(t, err, &protocol)
160- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)
158+ var protocolErr *protoErr
159+ require.ErrorAs(t, err, &protocolErr)
160+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
161161 d.mu.Lock()
162162 parked := d.parked[token]
163163 claimed := parked != nil && parked.claimed
M internal/usecase/daemon/route_identity_test_helpers_test.go +5 -5
@@ -2,15 +2,15 @@ package daemon
22
33 import (
44 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
66 )
77
8-func testRouteTarget(name string, marker byte) ports.ExactSessionTarget {
9- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
8+func testRouteTarget(name string, marker byte) protocol.ExactSessionTarget {
9+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
1010 }
1111
12-func testRouteEntry(key, generation uint64, name string, marker byte, kind ports.RouteKind) ports.RecentRouteEntry {
13- return ports.RecentRouteEntry{
12+func testRouteEntry(key, generation uint64, name string, marker byte, kind protocol.RouteKind) protocol.RecentRouteEntry {
13+ return protocol.RecentRouteEntry{
1414 Key: key, Generation: generation, Target: testRouteTarget(name, marker),
1515 Name: name, Kind: kind,
1616 }
M internal/usecase/daemon/route_position_test.go +5 -4
@@ -8,6 +8,7 @@ import (
88
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
@@ -28,7 +29,7 @@ func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
2829 sess.tabs = append(sess.tabs, second)
2930 sess.mu.Unlock()
3031 tr := &closeTrackingTransport{}
31- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
32+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
3233 hello := ports.Hello{
3334 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
3435 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
@@ -78,7 +79,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
7879 sess.tabs = append(sess.tabs, second)
7980 sess.mu.Unlock()
8081 tr := &closeTrackingTransport{}
81- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
82+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
8283 hello := ports.Hello{
8384 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
8485 Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
@@ -93,7 +94,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
9394 sess.selectAttachmentTab(ac, "tab-2")
9495 d.paint(sess, ac, false, nil)
9596
96- var positions []ports.RoutePosition
97+ var positions []protocol.RoutePosition
9798 for _, frame := range tr.Sends() {
9899 if frame.Type != ports.MsgRoutePosition {
99100 continue
@@ -102,7 +103,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
102103 require.NoError(t, err)
103104 positions = append(positions, position)
104105 }
105- require.Equal(t, []ports.RoutePosition{
106+ require.Equal(t, []protocol.RoutePosition{
106107 {Target: target, ActiveTabID: "tab-1"},
107108 {Target: target, ActiveTabID: "tab-2"},
108109 }, positions)
M internal/usecase/daemon/same_peer_switch.go +10 -9
@@ -3,9 +3,10 @@ package daemon
33 import (
44 "github.com/bnema/vev/internal/domain"
55 "github.com/bnema/vev/internal/ports"
6+ "github.com/bnema/vev/internal/protocol"
67 )
78
8-func (ac *attachedClient) offerSamePeerTarget(target ports.ExactSessionTarget) {
9+func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {
910 ac.samePeerOfferMu.Lock()
1011 ac.samePeerOffer = &target
1112 ac.samePeerOfferMu.Unlock()
@@ -20,7 +21,7 @@ func (ac *attachedClient) clearSamePeerOffer() {
2021 // consumeSamePeerOffer linearizes the client confirmation with the daemon's
2122 // endpoint-empty offer. A client cannot select an arbitrary local session by
2223 // manufacturing this frame.
23-func (ac *attachedClient) consumeSamePeerOffer(target ports.ExactSessionTarget) bool {
24+func (ac *attachedClient) consumeSamePeerOffer(target protocol.ExactSessionTarget) bool {
2425 ac.samePeerOfferMu.Lock()
2526 defer ac.samePeerOfferMu.Unlock()
2627 if ac.samePeerOffer == nil || *ac.samePeerOffer != target {
@@ -33,18 +34,18 @@ func (ac *attachedClient) consumeSamePeerOffer(target ports.ExactSessionTarget)
3334 // switchSamePeerForAttachment commits one client-confirmed endpoint-empty
3435 // target. The request never names an endpoint: its authority is the current
3536 // authenticated attachment plus the target's exact lifecycle identity.
36-func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request ports.SamePeerSwitchRequest) {
37+func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request protocol.SamePeerSwitchRequest) {
3738 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
3839 return
3940 }
4041 if !effect.ac.consumeSamePeerOffer(request.Target) {
41- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)
42+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
4243 return
4344 }
4445
4546 target, targetTabIndex, ok := d.samePeerTarget(request)
4647 if !ok || target == effect.sess {
47- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)
48+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
4849 return
4950 }
5051
@@ -67,7 +68,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
6768 ready: true,
6869 })
6970 if err != nil {
70- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)
71+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
7172 return
7273 }
7374
@@ -83,7 +84,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
8384 // samePeerTarget resolves the exact target and the client-owned tab cursor
8485 // under the daemon/session locks. A missing cursor deliberately falls back to
8586 // the target session's current default rather than leaking the source view.
86-func (d *Daemon) samePeerTarget(request ports.SamePeerSwitchRequest) (*session, int, bool) {
87+func (d *Daemon) samePeerTarget(request protocol.SamePeerSwitchRequest) (*session, int, bool) {
8788 d.mu.Lock()
8889 defer d.mu.Unlock()
8990
@@ -108,7 +109,7 @@ func (d *Daemon) samePeerTarget(request ports.SamePeerSwitchRequest) (*session,
108109 return target, tabIndex, true
109110 }
110111
111-func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code ports.SamePeerSwitchFailureCode) {
112+func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code protocol.SamePeerSwitchFailureCode) {
112113 if effect == nil || effect.ac == nil {
113114 return
114115 }
@@ -121,7 +122,7 @@ func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID u
121122 }
122123 defer sender.End()
123124 }
124- payload, err := ports.MarshalSamePeerSwitchFailure(ports.SamePeerSwitchFailure{RequestID: requestID, Code: code})
125+ payload, err := ports.MarshalSamePeerSwitchFailure(protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code})
125126 if err != nil {
126127 return
127128 }
M internal/usecase/daemon/same_peer_switch_test.go +13 -12
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/vev/internal/domain"
99 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1011 )
1112
1213 func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
@@ -26,7 +27,7 @@ func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
2627 d.mu.Lock()
2728 d.sessions[target.id] = target
2829 d.mu.Unlock()
29- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
30+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
3031
3132 token := beginRecentRoutePaletteEffect(t, d, source, ac)
3233 d.handleInputForAttachment(token, []byte("\x1b "))
@@ -35,9 +36,9 @@ func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
3536 targetFrame := awaitFrame(t, sends, ports.MsgAttachTarget)
3637 attachTarget, err := ports.UnmarshalAttachTarget(targetFrame.Payload)
3738 require.NoError(t, err)
38- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, attachTarget.ExactTarget)
39+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, attachTarget.ExactTarget)
3940
40- d.switchSamePeerForAttachment(token, ports.SamePeerSwitchRequest{
41+ d.switchSamePeerForAttachment(token, protocol.SamePeerSwitchRequest{
4142 RequestID: 1, Target: *attachTarget.ExactTarget,
4243 })
4344
@@ -68,16 +69,16 @@ func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) {
6869 d.mu.Lock()
6970 d.sessions[target.id] = target
7071 d.mu.Unlock()
71- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
72+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
7273
7374 token := source.captureAttachmentCapability(ac, ac.transport())
7475 effect, admitted := ac.beginAttachmentEffect(token)
7576 require.True(t, admitted)
7677 defer effect.End()
77- requestTarget := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
78+ requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
7879 ac.offerSamePeerTarget(requestTarget)
7980
80- d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{
81+ d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
8182 RequestID: 1,
8283 Target: requestTarget,
8384 PreferredTabID: domain.TabStableID(target.tabs[1].stableID),
@@ -88,13 +89,13 @@ func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) {
8889 identityFrame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
8990 identity, err := ports.UnmarshalCommittedRouteIdentity(identityFrame.Payload)
9091 require.NoError(t, err)
91- require.Equal(t, ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, identity.Target)
92+ require.Equal(t, protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, identity.Target)
9293 }
9394
9495 func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {
9596 d, sess, ac, _, releases := newManualTabSession(t, 1)
9697 defer releaseAll(releases)
97- ac.offerSamePeerTarget(ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "target"})
98+ ac.offerSamePeerTarget(protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "target"})
9899
99100 d.finishSendErrorDetach(sess, ac, ac.transport())
100101
@@ -106,21 +107,21 @@ func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {
106107 func TestSamePeerSwitchRejectsStaleTargetWithoutMutation(t *testing.T) {
107108 d, source, ac, sends, releases := newManualTabSession(t, 1)
108109 defer releaseAll(releases)
109- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
110+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
110111
111112 token := source.captureAttachmentCapability(ac, ac.transport())
112113 effect, admitted := ac.beginAttachmentEffect(token)
113114 require.True(t, admitted)
114115 defer effect.End()
115116
116- d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{
117+ d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
117118 RequestID: 1,
118- Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"},
119+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"},
119120 })
120121
121122 require.Same(t, source, ac.currentAttachmentSession())
122123 failureFrame := awaitFrame(t, sends, ports.MsgSamePeerSwitchFailure)
123124 failure, err := ports.UnmarshalSamePeerSwitchFailure(failureFrame.Payload)
124125 require.NoError(t, err)
125- require.Equal(t, ports.SamePeerSwitchFailure{RequestID: 1, Code: ports.SamePeerSwitchStaleTarget}, failure)
126+ require.Equal(t, protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchStaleTarget}, failure)
126127 }
M internal/usecase/daemon/session_back.go +2 -2
@@ -2,7 +2,7 @@ package daemon
22
33 import (
44 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
66 )
77
88 // backSessionForAttachment delegates previous-route ownership to the client
@@ -22,7 +22,7 @@ func (d *Daemon) backSessionForAttachment(effect *attachmentEffect) error {
2222 if snapshot.Previous.Key == 0 || snapshot.Previous.Generation == 0 {
2323 return nil
2424 }
25- return d.sendRecentRouteNavigationActionForAttachment(effect, ports.RouteNavigationAction{
25+ return d.sendRecentRouteNavigationActionForAttachment(effect, protocol.RouteNavigationAction{
2626 SnapshotGeneration: snapshot.Generation,
2727 Key: snapshot.Previous.Key,
2828 Generation: snapshot.Previous.Generation,
M internal/usecase/daemon/session_back_route_test.go +7 -6
@@ -4,6 +4,7 @@ import (
44 "testing"
55
66 "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
78 "github.com/stretchr/testify/require"
89 )
910
@@ -18,17 +19,17 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T
1819 effect, admitted := ac.beginAttachmentEffect(token)
1920 require.True(t, admitted)
2021 defer effect.End()
21- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
22+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
2223 Generation: 5,
23- Active: ports.RouteRef{Key: 9, Generation: 5},
24- Previous: ports.RouteRef{Key: 7, Generation: 3},
25- Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)},
24+ Active: protocol.RouteRef{Key: 9, Generation: 5},
25+ Previous: protocol.RouteRef{Key: 7, Generation: 3},
26+ Entries: []protocol.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, protocol.RouteKindLocal)},
2627 })
2728
2829 require.NoError(t, d.backSessionForAttachment(effect))
2930 frames := transport.Sends()
3031 require.NotEmpty(t, frames)
31- var action ports.RouteNavigationAction
32+ var action protocol.RouteNavigationAction
3233 found := false
3334 for _, frame := range frames {
3435 if frame.Type != ports.MsgNavigateRecentRoute {
@@ -40,5 +41,5 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T
4041 found = true
4142 }
4243 require.True(t, found)
43- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 5, Key: 7, Generation: 3}, action)
44+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 5, Key: 7, Generation: 3}, action)
4445 }
M internal/usecase/daemon/session_rename_purge_test.go +7 -6
@@ -8,6 +8,7 @@ import (
88 "github.com/bnema/vev/internal/domain"
99 "github.com/bnema/vev/internal/ports"
1010 portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
1112 )
1213
1314 func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) {
@@ -19,14 +20,14 @@ func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) {
1920 sess.mu.Unlock()
2021 token := sess.captureAttachmentCapability(ac, ac.transport())
2122 ac.installTestAttachmentCapability(token)
22- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
23+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
2324
2425 require.NoError(t, d.renameSession(sess, "vps-infra"))
2526
2627 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
2728 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)
2829 require.NoError(t, err)
29- require.Equal(t, ports.ExactSessionTarget{
30+ require.Equal(t, protocol.ExactSessionTarget{
3031 LifecycleID: domain.IncarnationID{1},
3132 SessionName: "vps-infra",
3233 }, identity.Target)
@@ -46,11 +47,11 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T)
4647 require.NoError(t, d.renameSession(sess, "vps-infra"))
4748 require.Empty(t, sends, "the route ledger must be published before its identity can be updated")
4849
49- activeRef := ports.RouteRef{Key: 1, Generation: 1}
50- snapshot := ports.RecentRouteSnapshot{
50+ activeRef := protocol.RouteRef{Key: 1, Generation: 1}
51+ snapshot := protocol.RecentRouteSnapshot{
5152 Generation: 1,
5253 Active: activeRef,
53- ActiveEntry: ports.RecentRouteEntry{Key: 1, Generation: 1, Target: testRouteTarget("0", 1), Name: "0", Kind: ports.RouteKindLocal},
54+ ActiveEntry: protocol.RecentRouteEntry{Key: 1, Generation: 1, Target: testRouteTarget("0", 1), Name: "0", Kind: protocol.RouteKindLocal},
5455 Home: activeRef,
5556 }
5657 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)
@@ -60,7 +61,7 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T)
6061 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
6162 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)
6263 require.NoError(t, err)
63- require.Equal(t, ports.ExactSessionTarget{
64+ require.Equal(t, protocol.ExactSessionTarget{
6465 LifecycleID: domain.IncarnationID{1},
6566 SessionName: "vps-infra",
6667 }, identity.Target)
M internal/usecase/daemon/status.go +6 -6
@@ -8,7 +8,7 @@ import (
88
99 renderer "github.com/bnema/vev-vt"
1010 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
1212 "github.com/bnema/vev/internal/usecase/command"
1313 "github.com/bnema/vev/internal/usecase/palette"
1414 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -189,7 +189,7 @@ func (s *session) statusSegments(includeTerminalTitle bool) statusSnapshot {
189189 }
190190
191191 func (s *session) statusSegmentsFor(ac *attachedClient, includeTerminalTitle bool) statusSnapshot {
192- var routeSnapshot ports.RecentRouteSnapshot
192+ var routeSnapshot protocol.RecentRouteSnapshot
193193 if ac != nil {
194194 routeSnapshot = ac.routeSnapshotCopy()
195195 }
@@ -199,7 +199,7 @@ func (s *session) statusSegmentsFor(ac *attachedClient, includeTerminalTitle boo
199199 presentation := recentRoutePresentation{name: s.name, ephemeral: s.ephemeral}
200200 if active, ok := activeRouteEntryForLifecycle(routeSnapshot, s.incarnation); ok {
201201 presentation.hostLabel = domain.RemoteDisplayOrigin(active.HostLabel)
202- if active.Kind == ports.RouteKindRemote {
202+ if active.Kind == protocol.RouteKindRemote {
203203 presentation.kind = recentRouteRemote
204204 }
205205 }
@@ -231,7 +231,7 @@ func tabDisplayName(tb *tab, index int) string {
231231 return strconv.Itoa(index + 1)
232232 }
233233
234-func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot ports.RecentRouteSnapshot) []rankedRecent {
234+func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot protocol.RecentRouteSnapshot) []rankedRecent {
235235 if hints == nil || hints.Kind != command.ContextHintRecentSessions {
236236 return nil
237237 }
@@ -270,10 +270,10 @@ func (d *Daemon) barStateForPaletteHints(cur *session, statusFeedback string, hi
270270 // attachment. Bar scripts remain local-session-only because
271271 // their existing execution contract depends on local tabs and PTYs.
272272 func (d *Daemon) barStateForAttachmentPaletteHints(cur *session, statusFeedback string, hints *palette.ContextualHints) barState {
273- return d.barStateForAttachmentPaletteHintsFor(cur, nil, statusFeedback, hints, ports.RecentRouteSnapshot{})
273+ return d.barStateForAttachmentPaletteHintsFor(cur, nil, statusFeedback, hints, protocol.RecentRouteSnapshot{})
274274 }
275275
276-func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attachedClient, statusFeedback string, hints *palette.ContextualHints, capturedRouteSnapshot ports.RecentRouteSnapshot) barState {
276+func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attachedClient, statusFeedback string, hints *palette.ContextualHints, capturedRouteSnapshot protocol.RecentRouteSnapshot) barState {
277277 routeSnapshot := capturedRouteSnapshot
278278 if routeSnapshot.Generation == 0 && ac != nil {
279279 routeSnapshot = ac.routeSnapshotCopy()
M internal/usecase/daemon/status_test.go +21 -20
@@ -14,6 +14,7 @@ import (
1414 renderer "github.com/bnema/vev-vt/ansi"
1515 "github.com/bnema/vev/internal/domain"
1616 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
1718 "github.com/bnema/vev/internal/usecase/command"
1819 "github.com/bnema/vev/internal/usecase/layout"
1920 "github.com/bnema/vev/internal/usecase/palette"
@@ -126,14 +127,14 @@ func TestStatusSegmentsResolvesActiveLifecyclePresentation(t *testing.T) {
126127
127128 sess.name = "vive"
128129 sess.incarnation = domain.SessionLifecycleID{1}
129- ref := ports.RouteRef{Key: 1, Generation: 1}
130- ac.setRouteSnapshot(ports.RecentRouteSnapshot{
130+ ref := protocol.RouteRef{Key: 1, Generation: 1}
131+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
131132 Generation: 1,
132133 Active: ref,
133- ActiveEntry: ports.RecentRouteEntry{
134+ ActiveEntry: protocol.RecentRouteEntry{
134135 Key: ref.Key, Generation: ref.Generation,
135- Target: ports.ExactSessionTarget{LifecycleID: tt.routeLifecycle, SessionName: sess.name},
136- Name: "vive", HostLabel: "user@arch", Kind: ports.RouteKindRemote,
136+ Target: protocol.ExactSessionTarget{LifecycleID: tt.routeLifecycle, SessionName: sess.name},
137+ Name: "vive", HostLabel: "user@arch", Kind: protocol.RouteKindRemote,
137138 },
138139 })
139140
@@ -466,7 +467,7 @@ func TestStatusApplyThemeStoresClientAndPropagatesScreens(t *testing.T) {
466467 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
467468 defer releasePTY1()
468469 defer releasePTY2()
469- msg := ports.Theme{HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, SchemeKnown: true, Light: true}
470+ msg := protocol.Theme{HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, SchemeKnown: true, Light: true}
470471
471472 d.applyTheme(sess, ac, msg)
472473
@@ -484,7 +485,7 @@ func TestApplyThemePropagatesToFloatingPane(t *testing.T) {
484485 tb := testAttachmentTab(sess)
485486 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
486487 installTestFloating(tb, floating, true)
487- clientTheme := ports.Theme{
488+ clientTheme := protocol.Theme{
488489 HasForeground: true,
489490 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
490491 HasBackground: true,
@@ -520,7 +521,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) {
520521 defer releasePTY2()
521522 d.ApplyConfig(domain.Config{Theme: tc.mode})
522523
523- d.applyTheme(sess, ac, ports.Theme{
524+ d.applyTheme(sess, ac, protocol.Theme{
524525 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
525526 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
526527 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,
@@ -575,7 +576,7 @@ func TestAutoThemeDetachClearsPaneColorScheme(t *testing.T) {
575576 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
576577 defer releasePTY()
577578 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})
578- d.applyTheme(sess, ac, ports.Theme{
579+ d.applyTheme(sess, ac, protocol.Theme{
579580 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
580581 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
581582 TrueColor: true, SchemeKnown: true, Light: true,
@@ -592,7 +593,7 @@ func TestAttachClientClearsStaleColorSchemeOnReplacement(t *testing.T) {
592593 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
593594 defer releasePTY()
594595 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})
595- d.applyTheme(sess, ac, ports.Theme{
596+ d.applyTheme(sess, ac, protocol.Theme{
596597 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
597598 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
598599 TrueColor: true, SchemeKnown: true, Light: true,
@@ -611,7 +612,7 @@ func TestForcedThemeDetachPreservesBuiltinOnPanes(t *testing.T) {
611612 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
612613 defer releasePTY()
613614 d.ApplyConfig(domain.Config{Theme: domain.ThemeDark})
614- d.applyTheme(sess, ac, ports.Theme{
615+ d.applyTheme(sess, ac, protocol.Theme{
615616 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
616617 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
617618 TrueColor: true, SchemeKnown: true, Light: true,
@@ -628,12 +629,12 @@ func TestApplyThemeAutoUnknownDoesNotClobberPaneColorScheme(t *testing.T) {
628629 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
629630 defer releasePTY()
630631
631- d.applyTheme(sess, ac, ports.Theme{
632+ d.applyTheme(sess, ac, protocol.Theme{
632633 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
633634 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
634635 TrueColor: true, SchemeKnown: true, Light: true,
635636 })
636- d.applyTheme(sess, ac, ports.Theme{
637+ d.applyTheme(sess, ac, protocol.Theme{
637638 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
638639 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
639640 TrueColor: true,
@@ -703,7 +704,7 @@ func TestAttachClientClearsStaleScreenDefaultColors(t *testing.T) {
703704 p, release := newBlockingPTY(t)
704705 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
705706 defer release()
706- d.applyTheme(sess, ac, ports.Theme{
707+ d.applyTheme(sess, ac, protocol.Theme{
707708 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
708709 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
709710 })
@@ -723,7 +724,7 @@ func TestClientGoneResetsScreenDefaultColors(t *testing.T) {
723724 p, release := newBlockingPTY(t)
724725 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
725726 defer release()
726- d.applyTheme(sess, ac, ports.Theme{
727+ d.applyTheme(sess, ac, protocol.Theme{
727728 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
728729 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
729730 })
@@ -747,7 +748,7 @@ func TestDetachOnSendErrorResetsScreenDefaultColors(t *testing.T) {
747748 p, release := newBlockingPTY(t)
748749 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
749750 defer release()
750- d.applyTheme(sess, ac, ports.Theme{
751+ d.applyTheme(sess, ac, protocol.Theme{
751752 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
752753 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
753754 })
@@ -766,7 +767,7 @@ func TestDetachOnSendErrorParkPreservesScreenDefaultColors(t *testing.T) {
766767 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
767768 defer release()
768769 ac.resumeCapable = true
769- d.applyTheme(sess, ac, ports.Theme{
770+ d.applyTheme(sess, ac, protocol.Theme{
770771 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
771772 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
772773 })
@@ -988,9 +989,9 @@ func TestBarStateForContextualRecentUsesClientSnapshot(t *testing.T) {
988989 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
989990 defer releasePTY()
990991
991- snapshot := ports.RecentRouteSnapshot{
992+ snapshot := protocol.RecentRouteSnapshot{
992993 Generation: 1,
993- Entries: []ports.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: ports.RouteKindLocal, Attention: true}},
994+ Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true}},
994995 }
995996 hints := palette.ContextualHints{
996997 Kind: command.ContextHintRecentSessions,
@@ -1444,7 +1445,7 @@ func TestStatusCoalescesCreateSwitchAndResize(t *testing.T) {
14441445 frameInput([]byte("\x1b ")),
14451446 frameInput([]byte("CNT\r")),
14461447 frameInput([]byte("\x1b1")),
1447- ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(ports.Resize{Size: domain.Size{Cols: 22, Rows: 6}})},
1448+ ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.Resize{Size: domain.Size{Cols: 22, Rows: 6}})},
14481449 )
14491450
14501451 var hg sync.WaitGroup
M internal/usecase/daemon/theme_cache_test.go +2 -2
@@ -7,7 +7,7 @@ import (
77
88 renderer "github.com/bnema/vev-vt"
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1111 "github.com/bnema/vev/internal/usecase/layout"
1212 themeui "github.com/bnema/vev/internal/usecase/theme"
1313 )
@@ -26,7 +26,7 @@ func cacheTheme() themeui.Theme {
2626 func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) {
2727 d, sess, ac, _ := newManualSessionWithPTYs(t)
2828 raw := cacheTheme()
29- d.applyTheme(sess, ac, ports.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown})
29+ d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown})
3030 before := ac.getAppliedTheme()
3131
3232 cfg := domain.Defaults()
M internal/usecase/daemon/theme_integration_test.go +3 -3
@@ -7,12 +7,12 @@ import (
77
88 renderer "github.com/bnema/vev-vt/ansi"
99 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
1111 themeui "github.com/bnema/vev/internal/usecase/theme"
1212 )
1313
14-func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) ports.Theme {
15- out := ports.Theme{
14+func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme {
15+ out := protocol.Theme{
1616 Foreground: foreground, Background: background,
1717 HasForeground: true, HasBackground: true, TrueColor: true,
1818 SchemeKnown: true,
M internal/usecase/daemon/wire.go +6 -5
@@ -4,14 +4,15 @@ package daemon
44 import (
55 "github.com/bnema/vev/internal/domain"
66 "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
78 )
89
910 func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
1011 s.mu.Lock()
11- var identity *ports.CommittedRouteIdentity
12+ var identity *protocol.CommittedRouteIdentity
1213 if s.incarnation != (domain.SessionLifecycleID{}) {
13- identity = &ports.CommittedRouteIdentity{
14- Target: ports.ExactSessionTarget{LifecycleID: s.incarnation, SessionName: s.name},
14+ identity = &protocol.CommittedRouteIdentity{
15+ Target: protocol.ExactSessionTarget{LifecycleID: s.incarnation, SessionName: s.name},
1516 Ephemeral: s.ephemeral,
1617 }
1718 }
@@ -26,7 +27,7 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
2627 w.ResumeToken = resumeToken
2728 payload := ports.MarshalWelcome(w)
2829 if payload == nil {
29- return ports.Frame{}, ports.ErrInvalidRouteWire
30+ return ports.Frame{}, protocol.ErrInvalidRouteWire
3031 }
3132 return ports.Frame{Type: ports.MsgWelcome, Payload: payload}, nil
3233 }
@@ -36,7 +37,7 @@ func frameError(code uint16, text string) ports.Frame {
3637 }
3738
3839 func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (ports.Frame, error) {
39- payload, err := ports.MarshalOutput(ports.Output{
40+ payload, err := ports.MarshalOutput(protocol.Output{
4041 Epoch: 1, Base: baseState, New: state, Echo: echoAck,
4142 Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: b,
4243 })
M internal/usecase/daemon/wire_test_helpers_test.go +7 -4
@@ -1,8 +1,11 @@
11 package daemon
22
3-import "github.com/bnema/vev/internal/ports"
3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
47
5-func mustMarshalOutput(m ports.Output) []byte {
8+func mustMarshalOutput(m protocol.Output) []byte {
69 payload, err := ports.MarshalOutput(m)
710 if err != nil {
811 panic(err)
@@ -10,7 +13,7 @@ func mustMarshalOutput(m ports.Output) []byte {
1013 return payload
1114 }
1215
13-func mustMarshalAck(m ports.Ack) []byte {
16+func mustMarshalAck(m protocol.Ack) []byte {
1417 payload, err := ports.MarshalAck(m)
1518 if err != nil {
1619 panic(err)
@@ -18,7 +21,7 @@ func mustMarshalAck(m ports.Ack) []byte {
1821 return payload
1922 }
2023
21-func mustMarshalResize(m ports.Resize) []byte {
24+func mustMarshalResize(m protocol.Resize) []byte {
2225 payload, err := ports.MarshalResize(m)
2326 if err != nil {
2427 panic(err)
M internal/usecase/palette/fuzzy_test.go +3 -3
@@ -4,7 +4,7 @@ import (
44 "testing"
55 "time"
66
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/bnema/vev/internal/usecase/command"
99 "github.com/stretchr/testify/require"
1010 )
@@ -87,7 +87,7 @@ func TestFuzzyOrdersMixedResults(t *testing.T) {
8787 wantText []string
8888 wantKinds []ResultKind
8989 wantPositions [][]int
90- wantActiveTargets map[int]ports.ExactSessionTarget
90+ wantActiveTargets map[int]protocol.ExactSessionTarget
9191 }{
9292 {
9393 name: "command shortcode precedes sessions and description matches",
@@ -128,7 +128,7 @@ func TestFuzzyOrdersMixedResults(t *testing.T) {
128128 wantText: []string{"AX", "Resume session aAlpha", "Resume session aBravo", "Switch to session aEcho", "Switch to session aEcho", "Switch to session aZulu"},
129129 wantKinds: []ResultKind{ResultKindCommand, ResultKindStoppedSession, ResultKindStoppedSession, ResultKindActiveSession, ResultKindActiveSession, ResultKindActiveSession},
130130 wantPositions: [][]int{{0}, {15}, {15}, {18}, {18}, {18}},
131- wantActiveTargets: map[int]ports.ExactSessionTarget{
131+ wantActiveTargets: map[int]protocol.ExactSessionTarget{
132132 3: testExactTarget("aEcho", 4), 4: testExactTarget("aEcho", 5), 5: testExactTarget("aZulu", 3),
133133 },
134134 },
M internal/usecase/palette/result.go +11 -11
@@ -4,7 +4,7 @@ import (
44 "time"
55
66 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
88 "github.com/bnema/vev/internal/usecase/command"
99 )
1010
@@ -32,7 +32,7 @@ type Result struct {
3232 type sessionPayload struct {
3333 display string
3434 createdAt time.Time
35- target ports.ExactSessionTarget
35+ target protocol.ExactSessionTarget
3636 }
3737
3838 type remoteSessionPayload struct {
@@ -43,7 +43,7 @@ type remoteSessionPayload struct {
4343
4444 type routePayload struct {
4545 label string
46- action ports.RouteNavigationAction
46+ action protocol.RouteNavigationAction
4747 }
4848
4949 // NewCommandResult creates a static command palette target.
@@ -52,13 +52,13 @@ func NewCommandResult(cmd command.Command) Result {
5252 }
5353
5454 // NewActiveSessionResult creates an immutable active named-session target.
55-func NewActiveSessionResult(target ports.ExactSessionTarget, createdAt time.Time) Result {
55+func NewActiveSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
5656 return NewActiveSessionResultWithDisplayOrigin(target, createdAt, "")
5757 }
5858
5959 // NewActiveSessionResultWithDisplayOrigin creates an active named-session target
6060 // qualified for presentation through a remote attachment origin.
61-func NewActiveSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
61+func NewActiveSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
6262 return Result{
6363 kind: ResultKindActiveSession,
6464 session: newSessionPayload(target, createdAt, displayOrigin),
@@ -66,20 +66,20 @@ func NewActiveSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, cr
6666 }
6767
6868 // NewStoppedSessionResult creates an immutable stopped named-session target.
69-func NewStoppedSessionResult(target ports.ExactSessionTarget, createdAt time.Time) Result {
69+func NewStoppedSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
7070 return NewStoppedSessionResultWithDisplayOrigin(target, createdAt, "")
7171 }
7272
7373 // NewStoppedSessionResultWithDisplayOrigin creates a stopped named-session target
7474 // qualified for presentation through a remote attachment origin.
75-func NewStoppedSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
75+func NewStoppedSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
7676 return Result{
7777 kind: ResultKindStoppedSession,
7878 session: newSessionPayload(target, createdAt, displayOrigin),
7979 }
8080 }
8181
82-func newSessionPayload(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) sessionPayload {
82+func newSessionPayload(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) sessionPayload {
8383 display := target.SessionName
8484 if displayOrigin != "" {
8585 display = domain.RemoteSessionDisplay(target.SessionName, displayOrigin)
@@ -98,7 +98,7 @@ func NewRemoteSessionResult(key domain.RemoteSessionKey, target domain.RemoteSes
9898 }
9999
100100 // NewRecentRouteResult creates an immutable client-ledger route target.
101-func NewRecentRouteResult(label string, action ports.RouteNavigationAction) Result {
101+func NewRecentRouteResult(label string, action protocol.RouteNavigationAction) Result {
102102 return Result{kind: ResultKindRecentRoute, route: routePayload{label: label, action: action}}
103103 }
104104
@@ -163,7 +163,7 @@ func (r Result) SessionCreatedAt() (time.Time, bool) {
163163
164164 // SessionTarget returns the exact lifecycle UUID and name for local active and
165165 // stopped session results.
166-func (r Result) SessionTarget() (ports.ExactSessionTarget, bool) {
166+func (r Result) SessionTarget() (protocol.ExactSessionTarget, bool) {
167167 return r.session.target, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession
168168 }
169169
@@ -187,7 +187,7 @@ func (r Result) RemoteSessionUnavailableReason() (string, bool) {
187187
188188 // RouteNavigationAction returns the exact client-ledger target only for a
189189 // recent-route result.
190-func (r Result) RouteNavigationAction() (ports.RouteNavigationAction, bool) {
190+func (r Result) RouteNavigationAction() (protocol.RouteNavigationAction, bool) {
191191 return r.route.action, r.kind == ResultKindRecentRoute
192192 }
193193
M internal/usecase/palette/result_test.go +4 -4
@@ -5,13 +5,13 @@ import (
55 "time"
66
77 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
99 "github.com/bnema/vev/internal/usecase/command"
1010 "github.com/stretchr/testify/require"
1111 )
1212
13-func testExactTarget(name string, marker byte) ports.ExactSessionTarget {
14- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
13+func testExactTarget(name string, marker byte) protocol.ExactSessionTarget {
14+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
1515 }
1616
1717 func TestResultKindsAndSessionLifecycleTargets(t *testing.T) {
@@ -73,7 +73,7 @@ func TestResultKindsAndSessionLifecycleTargets(t *testing.T) {
7373 }
7474
7575 func TestRecentRouteResultCarriesExactNavigationAction(t *testing.T) {
76- action := ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 7, Generation: 3}
76+ action := protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 7, Generation: 3}
7777 result := NewRecentRouteResult("logs@edge", action)
7878
7979 require.Equal(t, ResultKindRecentRoute, result.Kind())
M scripts/remote-picker-harness/main.go +3 -2
@@ -20,6 +20,7 @@ import (
2020 "github.com/bnema/vev/internal/adapters/sshstdio"
2121 "github.com/bnema/vev/internal/domain"
2222 "github.com/bnema/vev/internal/ports"
23+ "github.com/bnema/vev/internal/protocol"
2324 )
2425
2526 const (
@@ -660,7 +661,7 @@ func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello
660661 return fmt.Errorf("welcome frame type %d", frame.Type)
661662 }
662663 tr.probe.recordControl(probeEventWelcome)
663- if err := tr.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{TrueColor: hello.TrueColor, SchemeKnown: true})}); err != nil {
664+ if err := tr.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{TrueColor: hello.TrueColor, SchemeKnown: true})}); err != nil {
664665 return fmt.Errorf("send initial theme: %w", err)
665666 }
666667 return nil
@@ -756,7 +757,7 @@ func awaitRemotePickerPreview(ctx context.Context, tr *harnessTransport, marker
756757 }
757758 }
758759
759-func remotePreviewContains(preview ports.RemotePreview, want string) bool {
760+func remotePreviewContains(preview protocol.RemotePreview, want string) bool {
760761 var text strings.Builder
761762 for _, cell := range preview.Cells {
762763 if !cell.Continuation {
M scripts/remote-picker-harness/visual_probe.go +6 -5
@@ -12,6 +12,7 @@ import (
1212 vt "github.com/bnema/vev-vt"
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
1516 )
1617
1718 const (
@@ -38,7 +39,7 @@ type visualOutputState struct {
3839 initialized bool
3940 }
4041
41-func (s visualOutputState) next(output ports.Output) (visualOutputState, bool) {
42+func (s visualOutputState) next(output protocol.Output) (visualOutputState, bool) {
4243 if output.Epoch == 0 {
4344 return visualOutputState{}, false
4445 }
@@ -102,7 +103,7 @@ type visualCheckpoint struct {
102103 type visualOutputResult struct {
103104 Accepted bool
104105 StateBearing bool
105- Ack ports.Ack
106+ Ack protocol.Ack
106107 event *probeEvent
107108 }
108109
@@ -180,13 +181,13 @@ func (p *visualProbe) recordControl(kind string) {
180181 p.record(probeEvent{Kind: kind, Accepted: true})
181182 }
182183
183-func (p *visualProbe) apply(output ports.Output) visualOutputResult {
184+func (p *visualProbe) apply(output protocol.Output) visualOutputResult {
184185 if p == nil {
185186 return visualOutputResult{}
186187 }
187188 p.outputFrames++
188189 p.outputBytes += len(output.Data)
189- accepted := ports.ValidateOutput(output) == nil
190+ accepted := protocol.ValidateOutput(output) == nil
190191 next := visualOutputState{}
191192 if accepted {
192193 next, accepted = p.state.next(output)
@@ -220,7 +221,7 @@ func (p *visualProbe) apply(output ports.Output) visualOutputResult {
220221 p.checkpoints = append(p.checkpoints, checkpoint)
221222 result := visualOutputResult{Accepted: true, StateBearing: output.New != 0, event: event}
222223 if result.StateBearing {
223- result.Ack = ports.Ack{Epoch: output.Epoch, State: output.New}
224+ result.Ack = protocol.Ack{Epoch: output.Epoch, State: output.New}
224225 }
225226 return result
226227 }
M scripts/remote-picker-harness/visual_probe_test.go +27 -26
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
1213 "github.com/stretchr/testify/require"
1314 )
1415
@@ -29,7 +30,7 @@ func (t *probeTestTransport) Send(frame ports.Frame) error {
2930 func (*probeTestTransport) Recv() (ports.Frame, error) { return ports.Frame{}, errors.New("not used") }
3031 func (*probeTestTransport) Close() error { return nil }
3132
32-func probeTestOutput(t *testing.T, output ports.Output) ports.Frame {
33+func probeTestOutput(t *testing.T, output protocol.Output) ports.Frame {
3334 t.Helper()
3435 payload, err := ports.MarshalOutput(output)
3536 if err != nil {
@@ -43,19 +44,19 @@ func TestVisualProbePersistsFullAndIncrementalOutput(t *testing.T) {
4344 probe := newVisualProbe(size)
4445 for _, tt := range []struct {
4546 name string
46- output ports.Output
47+ output protocol.Output
4748 want string
4849 state uint64
4950 }{
5051 {
5152 name: "full",
52- output: ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
53+ output: protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
5354 want: "full",
5455 state: 1,
5556 },
5657 {
5758 name: "incremental",
58- output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte(" incremental")},
59+ output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte(" incremental")},
5960 want: "full incremental",
6061 state: 2,
6162 },
@@ -87,7 +88,7 @@ func TestVisualProbePersistsFullAndIncrementalOutput(t *testing.T) {
8788 func TestVisualProbeResizesToAcceptedOutput(t *testing.T) {
8889 probe := newVisualProbe(domain.Size{Cols: probeTestCols, Rows: probeTestRows})
8990 outputSize := domain.Size{Cols: 48, Rows: 3}
90- result := probe.apply(ports.Output{
91+ result := probe.apply(protocol.Output{
9192 Epoch: 1, New: 1, Full: true, ViewRevision: 1, Size: outputSize, Data: []byte("resized"),
9293 })
9394
@@ -103,30 +104,30 @@ func TestVisualProbeRejectsOutputWithoutMutationOrAck(t *testing.T) {
103104 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
104105 for _, tt := range []struct {
105106 name string
106- output ports.Output
107+ output protocol.Output
107108 }{
108109 {
109110 name: "base gap",
110- output: ports.Output{Epoch: 1, Base: 0, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("rejected")},
111+ output: protocol.Output{Epoch: 1, Base: 0, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("rejected")},
111112 },
112113 {
113114 name: "revision gap",
114- output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 5, Size: size, Data: []byte("rejected")},
115+ output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 5, Size: size, Data: []byte("rejected")},
115116 },
116117 {
117118 name: "stale epoch",
118- output: ports.Output{Epoch: 0, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte("rejected")},
119+ output: protocol.Output{Epoch: 0, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte("rejected")},
119120 },
120121 } {
121122 t.Run(tt.name, func(t *testing.T) {
122123 probe := newVisualProbe(size)
123- first := ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("keep")}
124+ first := protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("keep")}
124125 if result := probe.apply(first); !result.Accepted {
125126 t.Fatal("seed output was rejected")
126127 }
127128 beforeText, beforeState, beforeCheckpoints := probe.text(), probe.state, len(probe.checkpoints)
128129 result := probe.apply(tt.output)
129- if result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {
130+ if result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
130131 t.Fatalf("result = %+v, want rejected output without ACK", result)
131132 }
132133 if got := probe.text(); got != beforeText {
@@ -144,13 +145,13 @@ func TestVisualProbeRejectsOutputWithoutMutationOrAck(t *testing.T) {
144145
145146 func TestVisualProbeRejectsInvalidUninitializedSideEffects(t *testing.T) {
146147 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
147- for _, output := range []ports.Output{
148+ for _, output := range []protocol.Output{
148149 {Epoch: 1, Base: 1, Size: size, Data: []byte("invalid base")},
149150 {Epoch: 1, Full: true, Size: size, Data: []byte("invalid full")},
150151 } {
151152 probe := newVisualProbe(size)
152153 result := probe.apply(output)
153- if result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {
154+ if result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
154155 t.Fatalf("result = %+v, want rejected uninitialized side effect", result)
155156 }
156157 require.Len(t, probe.events, 1)
@@ -163,17 +164,17 @@ func TestVisualProbeSideEffectsApplyWithoutAck(t *testing.T) {
163164 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
164165 for _, tt := range []struct {
165166 name string
166- seed *ports.Output
167- effect ports.Output
167+ seed *protocol.Output
168+ effect protocol.Output
168169 }{
169170 {
170171 name: "before state-bearing output",
171- effect: ports.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
172+ effect: protocol.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
172173 },
173174 {
174175 name: "after state-bearing output",
175- seed: &ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
176- effect: ports.Output{Epoch: 1, ViewRevision: 4, Size: size, Data: []byte(" side effect")},
176+ seed: &protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
177+ effect: protocol.Output{Epoch: 1, ViewRevision: 4, Size: size, Data: []byte(" side effect")},
177178 },
178179 } {
179180 t.Run(tt.name, func(t *testing.T) {
@@ -183,7 +184,7 @@ func TestVisualProbeSideEffectsApplyWithoutAck(t *testing.T) {
183184 }
184185 beforeState, beforeCheckpoints := probe.state, len(probe.checkpoints)
185186 result := probe.apply(tt.effect)
186- if !result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {
187+ if !result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
187188 t.Fatalf("result = %+v, want accepted side effect without ACK", result)
188189 }
189190 if !strings.Contains(probe.text(), string(tt.effect.Data)) {
@@ -203,24 +204,24 @@ func TestProcessOutputFrameACKsOnlyAcceptedStateBearingFrames(t *testing.T) {
203204 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
204205 for _, tt := range []struct {
205206 name string
206- seed *ports.Output
207- output ports.Output
207+ seed *protocol.Output
208+ output protocol.Output
208209 wantACKs int
209210 wantResets int
210211 }{
211212 {
212213 name: "accepted full",
213- output: ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
214+ output: protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
214215 wantACKs: 1,
215216 },
216217 {
217218 name: "accepted side effect",
218- output: ports.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
219+ output: protocol.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
219220 },
220221 {
221222 name: "rejected state",
222- seed: &ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
223- output: ports.Output{Epoch: 1, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("gap")},
223+ seed: &protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
224+ output: protocol.Output{Epoch: 1, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("gap")},
224225 wantResets: 1,
225226 },
226227 } {
@@ -307,7 +308,7 @@ func TestHarnessArtifactIsBoundedAndContainsOnlyMetadata(t *testing.T) {
307308 artifact.registerProbe(probe)
308309 secret := "not-for-artifact"
309310 for state := uint64(1); state <= maxProbeEvents+1; state++ {
310- output := ports.Output{Epoch: 1, Base: state - 1, New: state, Size: size, Data: []byte(secret)}
311+ output := protocol.Output{Epoch: 1, Base: state - 1, New: state, Size: size, Data: []byte(secret)}
311312 if state == 1 {
312313 output.Full = true
313314 }
M transport_replay_integration_test.go +3 -2
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/vev/internal/adapters/ipc"
1212 "github.com/bnema/vev/internal/adapters/sshstdio"
1313 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
1415 "github.com/bnema/vev/internal/testutil/replaytest"
1516 "github.com/stretchr/testify/require"
1617 )
@@ -53,7 +54,7 @@ func TestTransportReplayIntegration(t *testing.T) {
5354 // SSH-stdio transports. The transports only carry frames; neither can infer
5455 // or alter the palette lifecycle.
5556 func TestThemeGenerationTransportSequences(t *testing.T) {
56- cleared := ports.Theme{
57+ cleared := protocol.Theme{
5758 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
5859 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
5960 TrueColor: true, SchemeKnown: true,
@@ -62,7 +63,7 @@ func TestThemeGenerationTransportSequences(t *testing.T) {
6263 definitive := cleared
6364 definitive.Palette = palette
6465 definitive.PaletteKnown = 1<<2 | 1<<10
65- sequence := []ports.Theme{cleared, definitive}
66+ sequence := []protocol.Theme{cleared, definitive}
6667
6768 tests := []struct {
6869 name string

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