detached 01fd03b

refactor(session): rename lifecycle states

commit
01fd03bdf583da7890aad807a44133cb0ee60a2c
author
brice · · 2026-08-07 09:49 UTC
parent
16ea2ba
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
31 files · +207 -150
M internal/adapters/remote/catalog.go +1 -1
@@ -185,7 +185,7 @@ func rejectTrailingJSON(dec *json.Decoder) error {
185185
186186 func validCatalogState(state string) bool {
187187 switch state {
188- case "running", "stopped", "broken":
188+ case "up", "down", "broken":
189189 return true
190190 default:
191191 return false
@@ -185,7 +185,7 @@ func rejectTrailingJSON(dec *json.Decoder) error {
185 185
186 func validCatalogState(state string) bool {186 func validCatalogState(state string) bool {
187 switch state {187 switch state {
188- case "running", "stopped", "broken":188+ case "up", "down", "broken":
189 return true189 return true
190 default:190 default:
191 return false191 return false
M internal/adapters/remote/catalog_cache_test.go +5 -5
@@ -30,8 +30,8 @@ func TestCatalogCacheStoreLoad(t *testing.T) {
3030 Host: "zebra",
3131 FetchedAt: fetchedAt,
3232 Sessions: []ports.RemoteCatalogSession{
33- {Name: "work", State: "running", Tabs: 2},
34- {Name: "alpha", State: "stopped", Ephemeral: true, Tabs: 1, Attached: true},
33+ {Name: "work", State: "up", Tabs: 2},
34+ {Name: "alpha", State: "down", Ephemeral: true, Tabs: 1, Attached: true},
3535 },
3636 },
3737 {
@@ -72,8 +72,8 @@ func TestCatalogCacheStoreLoad(t *testing.T) {
7272 Host: "zebra",
7373 FetchedAt: fetchedAt,
7474 Sessions: []ports.RemoteCatalogSession{
75- {Name: "alpha", State: "stopped", Ephemeral: true, Tabs: 1, Attached: true},
76- {Name: "work", State: "running", Tabs: 2},
75+ {Name: "alpha", State: "down", Ephemeral: true, Tabs: 1, Attached: true},
76+ {Name: "work", State: "up", Tabs: 2},
7777 },
7878 },
7979 }
@@ -122,7 +122,7 @@ func TestCatalogCacheLoadRejectsInvalidFilesWithoutReplacingThem(t *testing.T) {
122122 {name: "zero fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":0,"sessions":[]}]}`)},
123123 {name: "negative fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":-1,"sessions":[]}]}`)},
124124 {name: "duplicate hosts", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[]},{"target":"arch","fetched_at_unix_nano":2,"sessions":[]}]}`)},
125- {name: "duplicate sessions", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[{"name":"work","state":"running","ephemeral":false,"tabs":1,"attached":false},{"name":"work","state":"running","ephemeral":false,"tabs":1,"attached":false}]}]}`)},
125+ {name: "duplicate sessions", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[{"name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false},{"name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}]}`)},
126126 {name: "invalid utf8", raw: []byte("{\"version\":1,\"hosts\":[{\"target\":\"\xff\",\"fetched_at_unix_nano\":1,\"sessions\":[]}]}")},
127127 }
128128 for _, tc := range cases {
@@ -30,8 +30,8 @@ func TestCatalogCacheStoreLoad(t *testing.T) {
30 Host: "zebra",30 Host: "zebra",
31 FetchedAt: fetchedAt,31 FetchedAt: fetchedAt,
32 Sessions: []ports.RemoteCatalogSession{32 Sessions: []ports.RemoteCatalogSession{
33- {Name: "work", State: "running", Tabs: 2},33+ {Name: "work", State: "up", Tabs: 2},
34- {Name: "alpha", State: "stopped", Ephemeral: true, Tabs: 1, Attached: true},34+ {Name: "alpha", State: "down", Ephemeral: true, Tabs: 1, Attached: true},
35 },35 },
36 },36 },
37 {37 {
@@ -72,8 +72,8 @@ func TestCatalogCacheStoreLoad(t *testing.T) {
72 Host: "zebra",72 Host: "zebra",
73 FetchedAt: fetchedAt,73 FetchedAt: fetchedAt,
74 Sessions: []ports.RemoteCatalogSession{74 Sessions: []ports.RemoteCatalogSession{
75- {Name: "alpha", State: "stopped", Ephemeral: true, Tabs: 1, Attached: true},75+ {Name: "alpha", State: "down", Ephemeral: true, Tabs: 1, Attached: true},
76- {Name: "work", State: "running", Tabs: 2},76+ {Name: "work", State: "up", Tabs: 2},
77 },77 },
78 },78 },
79 }79 }
@@ -122,7 +122,7 @@ func TestCatalogCacheLoadRejectsInvalidFilesWithoutReplacingThem(t *testing.T) {
122 {name: "zero fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":0,"sessions":[]}]}`)},122 {name: "zero fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":0,"sessions":[]}]}`)},
123 {name: "negative fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":-1,"sessions":[]}]}`)},123 {name: "negative fetched at", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":-1,"sessions":[]}]}`)},
124 {name: "duplicate hosts", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[]},{"target":"arch","fetched_at_unix_nano":2,"sessions":[]}]}`)},124 {name: "duplicate hosts", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[]},{"target":"arch","fetched_at_unix_nano":2,"sessions":[]}]}`)},
125- {name: "duplicate sessions", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[{"name":"work","state":"running","ephemeral":false,"tabs":1,"attached":false},{"name":"work","state":"running","ephemeral":false,"tabs":1,"attached":false}]}]}`)},125+ {name: "duplicate sessions", raw: []byte(`{"version":1,"hosts":[{"target":"arch","fetched_at_unix_nano":1,"sessions":[{"name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false},{"name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}]}`)},
126 {name: "invalid utf8", raw: []byte("{\"version\":1,\"hosts\":[{\"target\":\"\xff\",\"fetched_at_unix_nano\":1,\"sessions\":[]}]}")},126 {name: "invalid utf8", raw: []byte("{\"version\":1,\"hosts\":[{\"target\":\"\xff\",\"fetched_at_unix_nano\":1,\"sessions\":[]}]}")},
127 }127 }
128 for _, tc := range cases {128 for _, tc := range cases {
M internal/adapters/remote/catalog_test.go +2 -2
@@ -83,7 +83,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
8383 valid := ports.RemoteCatalog{
8484 ProtocolVersion: ports.ProtocolVersion,
8585 Sessions: []ports.RemoteCatalogSession{
86- {Name: "work", State: "running", Ephemeral: false, Tabs: 2, Attached: true},
86+ {Name: "work", State: "up", Ephemeral: false, Tabs: 2, Attached: true},
8787 },
8888 }
8989 validJSON := mustCatalogJSON(t, valid)
@@ -148,7 +148,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
148148 },
149149 {
150150 name: "invalid session name",
151- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "bad name", State: "running"}}}),
151+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "bad name", State: "up"}}}),
152152 wantErr: errCatalogSession,
153153 },
154154 {
@@ -83,7 +83,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
83 valid := ports.RemoteCatalog{83 valid := ports.RemoteCatalog{
84 ProtocolVersion: ports.ProtocolVersion,84 ProtocolVersion: ports.ProtocolVersion,
85 Sessions: []ports.RemoteCatalogSession{85 Sessions: []ports.RemoteCatalogSession{
86- {Name: "work", State: "running", Ephemeral: false, Tabs: 2, Attached: true},86+ {Name: "work", State: "up", Ephemeral: false, Tabs: 2, Attached: true},
87 },87 },
88 }88 }
89 validJSON := mustCatalogJSON(t, valid)89 validJSON := mustCatalogJSON(t, valid)
@@ -148,7 +148,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
148 },148 },
149 {149 {
150 name: "invalid session name",150 name: "invalid session name",
151- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "bad name", State: "running"}}}),151+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "bad name", State: "up"}}}),
152 wantErr: errCatalogSession,152 wantErr: errCatalogSession,
153 },153 },
154 {154 {
M internal/app/remote_hosts.go +4 -4
@@ -205,10 +205,10 @@ func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) [
205205 Attached: session.Attached,
206206 }
207207 switch session.State {
208- case "running":
209- info.State = ports.SessionRunning
210- case "stopped":
211- info.State = ports.SessionStopped
208+ case "up":
209+ info.State = ports.SessionUp
210+ case "down":
211+ info.State = ports.SessionDown
212212 case "broken":
213213 info.State = ports.SessionBroken
214214 default:
@@ -205,10 +205,10 @@ func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) [
205 Attached: session.Attached,205 Attached: session.Attached,
206 }206 }
207 switch session.State {207 switch session.State {
208- case "running":208+ case "up":
209- info.State = ports.SessionRunning209+ info.State = ports.SessionUp
210- case "stopped":210+ case "down":
211- info.State = ports.SessionStopped211+ info.State = ports.SessionDown
212 case "broken":212 case "broken":
213 info.State = ports.SessionBroken213 info.State = ports.SessionBroken
214 default:214 default:
M internal/app/remote_test.go +9 -9
@@ -171,7 +171,7 @@ func TestRemoteHostListingUsesUnifiedStore(t *testing.T) {
171171 store := portsmocks.NewMockRemoteHostStore(t)
172172 store.EXPECT().Hosts().Return([]string{"arch"}, []string{"beta"}, nil).Once()
173173 catalog := portsmocks.NewMockRemoteCatalogClient(t)
174- catalog.EXPECT().List(mock.Anything, "arch").Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "build", State: "running"}}}, nil).Once()
174+ catalog.EXPECT().List(mock.Anything, "arch").Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "build", State: "up"}}}, nil).Once()
175175 var out bytes.Buffer
176176 err := runRemoteList(context.Background(), command{listHost: "arch"}, remoteHostDeps{
177177 store: store,
@@ -194,7 +194,7 @@ func TestListAllSessionsInvariants(t *testing.T) {
194194 var out bytes.Buffer
195195 err := listAllSessions(context.Background(), remoteHostDeps{
196196 localList: func(context.Context) ([]ports.SessionInfo, error) {
197- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, localErr
197+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, localErr
198198 },
199199 stdout: &out,
200200 }, nil)
@@ -210,13 +210,13 @@ func TestListAllSessionsInvariants(t *testing.T) {
210210 catalog := portsmocks.NewMockRemoteCatalogClient(t)
211211 catalog.EXPECT().List(mock.Anything, "arch").RunAndReturn(func(context.Context, string) (ports.RemoteCatalog, error) {
212212 cancel()
213- return ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "running"}}}, nil
213+ return ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil
214214 }).Once()
215215 var out bytes.Buffer
216216 err := listAllSessions(ctx, remoteHostDeps{
217217 catalog: catalog,
218218 localList: func(context.Context) ([]ports.SessionInfo, error) {
219- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, nil
219+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
220220 },
221221 stdout: &out,
222222 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -233,15 +233,15 @@ func TestListAllSessionsInvariants(t *testing.T) {
233233 var calls []string
234234 catalog.EXPECT().List(mock.Anything, "arch").Run(func(_ context.Context, target string) {
235235 calls = append(calls, target)
236- }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "running"}}}, nil).Once()
236+ }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil).Once()
237237 catalog.EXPECT().List(mock.Anything, "mule").Run(func(_ context.Context, target string) {
238238 calls = append(calls, target)
239- }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "ops", State: "running"}}}, nil).Once()
239+ }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "ops", State: "up"}}}, nil).Once()
240240 var out bytes.Buffer
241241 err := listAllSessions(context.Background(), remoteHostDeps{
242242 catalog: catalog,
243243 localList: func(context.Context) ([]ports.SessionInfo, error) {
244- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, nil
244+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
245245 },
246246 stdout: &out,
247247 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -265,8 +265,8 @@ func TestCatalogSessionsAsInfoInvariants(t *testing.T) {
265265 wantState ports.SessionState
266266 wantTabs uint16
267267 }{
268- {name: "running", session: ports.RemoteCatalogSession{Name: "dev", State: "running", Tabs: 2}, wantState: ports.SessionRunning, wantTabs: 2},
269- {name: "stopped", session: ports.RemoteCatalogSession{Name: "dev", State: "stopped", Tabs: 2}, wantState: ports.SessionStopped, wantTabs: 2},
268+ {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: 2}, wantState: ports.SessionUp, wantTabs: 2},
269+ {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: 2}, wantState: ports.SessionDown, wantTabs: 2},
270270 {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},
271271 {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},
272272 }
@@ -171,7 +171,7 @@ func TestRemoteHostListingUsesUnifiedStore(t *testing.T) {
171 store := portsmocks.NewMockRemoteHostStore(t)171 store := portsmocks.NewMockRemoteHostStore(t)
172 store.EXPECT().Hosts().Return([]string{"arch"}, []string{"beta"}, nil).Once()172 store.EXPECT().Hosts().Return([]string{"arch"}, []string{"beta"}, nil).Once()
173 catalog := portsmocks.NewMockRemoteCatalogClient(t)173 catalog := portsmocks.NewMockRemoteCatalogClient(t)
174- catalog.EXPECT().List(mock.Anything, "arch").Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "build", State: "running"}}}, nil).Once()174+ catalog.EXPECT().List(mock.Anything, "arch").Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "build", State: "up"}}}, nil).Once()
175 var out bytes.Buffer175 var out bytes.Buffer
176 err := runRemoteList(context.Background(), command{listHost: "arch"}, remoteHostDeps{176 err := runRemoteList(context.Background(), command{listHost: "arch"}, remoteHostDeps{
177 store: store,177 store: store,
@@ -194,7 +194,7 @@ func TestListAllSessionsInvariants(t *testing.T) {
194 var out bytes.Buffer194 var out bytes.Buffer
195 err := listAllSessions(context.Background(), remoteHostDeps{195 err := listAllSessions(context.Background(), remoteHostDeps{
196 localList: func(context.Context) ([]ports.SessionInfo, error) {196 localList: func(context.Context) ([]ports.SessionInfo, error) {
197- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, localErr197+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, localErr
198 },198 },
199 stdout: &out,199 stdout: &out,
200 }, nil)200 }, nil)
@@ -210,13 +210,13 @@ func TestListAllSessionsInvariants(t *testing.T) {
210 catalog := portsmocks.NewMockRemoteCatalogClient(t)210 catalog := portsmocks.NewMockRemoteCatalogClient(t)
211 catalog.EXPECT().List(mock.Anything, "arch").RunAndReturn(func(context.Context, string) (ports.RemoteCatalog, error) {211 catalog.EXPECT().List(mock.Anything, "arch").RunAndReturn(func(context.Context, string) (ports.RemoteCatalog, error) {
212 cancel()212 cancel()
213- return ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "running"}}}, nil213+ return ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil
214 }).Once()214 }).Once()
215 var out bytes.Buffer215 var out bytes.Buffer
216 err := listAllSessions(ctx, remoteHostDeps{216 err := listAllSessions(ctx, remoteHostDeps{
217 catalog: catalog,217 catalog: catalog,
218 localList: func(context.Context) ([]ports.SessionInfo, error) {218 localList: func(context.Context) ([]ports.SessionInfo, error) {
219- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, nil219+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
220 },220 },
221 stdout: &out,221 stdout: &out,
222 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})222 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -233,15 +233,15 @@ func TestListAllSessionsInvariants(t *testing.T) {
233 var calls []string233 var calls []string
234 catalog.EXPECT().List(mock.Anything, "arch").Run(func(_ context.Context, target string) {234 catalog.EXPECT().List(mock.Anything, "arch").Run(func(_ context.Context, target string) {
235 calls = append(calls, target)235 calls = append(calls, target)
236- }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "running"}}}, nil).Once()236+ }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil).Once()
237 catalog.EXPECT().List(mock.Anything, "mule").Run(func(_ context.Context, target string) {237 catalog.EXPECT().List(mock.Anything, "mule").Run(func(_ context.Context, target string) {
238 calls = append(calls, target)238 calls = append(calls, target)
239- }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "ops", State: "running"}}}, nil).Once()239+ }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "ops", State: "up"}}}, nil).Once()
240 var out bytes.Buffer240 var out bytes.Buffer
241 err := listAllSessions(context.Background(), remoteHostDeps{241 err := listAllSessions(context.Background(), remoteHostDeps{
242 catalog: catalog,242 catalog: catalog,
243 localList: func(context.Context) ([]ports.SessionInfo, error) {243 localList: func(context.Context) ([]ports.SessionInfo, error) {
244- return []ports.SessionInfo{{Name: "local", State: ports.SessionRunning}}, nil244+ return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil
245 },245 },
246 stdout: &out,246 stdout: &out,
247 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})247 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -265,8 +265,8 @@ func TestCatalogSessionsAsInfoInvariants(t *testing.T) {
265 wantState ports.SessionState265 wantState ports.SessionState
266 wantTabs uint16266 wantTabs uint16
267 }{267 }{
268- {name: "running", session: ports.RemoteCatalogSession{Name: "dev", State: "running", Tabs: 2}, wantState: ports.SessionRunning, wantTabs: 2},268+ {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: 2}, wantState: ports.SessionUp, wantTabs: 2},
269- {name: "stopped", session: ports.RemoteCatalogSession{Name: "dev", State: "stopped", Tabs: 2}, wantState: ports.SessionStopped, wantTabs: 2},269+ {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: 2}, wantState: ports.SessionDown, wantTabs: 2},
270 {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},270 {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},
271 {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},271 {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: 2}, wantState: ports.SessionBroken, wantTabs: 2},
272 }272 }
M internal/app/run.go +4 -4
@@ -1434,7 +1434,7 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
14341434 }
14351435 infos := make([]ports.SessionInfo, 0, len(records))
14361436 for _, r := range records {
1437- state := ports.SessionStopped
1437+ state := ports.SessionDown
14381438 if r.DegradedReason != "" {
14391439 state = ports.SessionBroken
14401440 }
@@ -1481,12 +1481,12 @@ func printSessions(w io.Writer, sessions []ports.SessionInfo) {
14811481 tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
14821482 _, _ = fmt.Fprintln(tw, "NAME\tSTATE\tTABS\tATTACHED")
14831483 for _, s := range sessions {
1484- state := "running"
1484+ state := "up"
14851485 tabs := fmt.Sprintf("%d", s.Tabs)
14861486 attached := "no"
14871487 switch s.State {
1488- case ports.SessionStopped:
1489- state = "stopped"
1488+ case ports.SessionDown:
1489+ state = "down"
14901490 tabs = "-"
14911491 case ports.SessionBroken:
14921492 state = "broken"
@@ -1434,7 +1434,7 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
1434 }1434 }
1435 infos := make([]ports.SessionInfo, 0, len(records))1435 infos := make([]ports.SessionInfo, 0, len(records))
1436 for _, r := range records {1436 for _, r := range records {
1437- state := ports.SessionStopped1437+ state := ports.SessionDown
1438 if r.DegradedReason != "" {1438 if r.DegradedReason != "" {
1439 state = ports.SessionBroken1439 state = ports.SessionBroken
1440 }1440 }
@@ -1481,12 +1481,12 @@ func printSessions(w io.Writer, sessions []ports.SessionInfo) {
1481 tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)1481 tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
1482 _, _ = fmt.Fprintln(tw, "NAME\tSTATE\tTABS\tATTACHED")1482 _, _ = fmt.Fprintln(tw, "NAME\tSTATE\tTABS\tATTACHED")
1483 for _, s := range sessions {1483 for _, s := range sessions {
1484- state := "running"1484+ state := "up"
1485 tabs := fmt.Sprintf("%d", s.Tabs)1485 tabs := fmt.Sprintf("%d", s.Tabs)
1486 attached := "no"1486 attached := "no"
1487 switch s.State {1487 switch s.State {
1488- case ports.SessionStopped:1488+ case ports.SessionDown:
1489- state = "stopped"1489+ state = "down"
1490 tabs = "-"1490 tabs = "-"
1491 case ports.SessionBroken:1491 case ports.SessionBroken:
1492 state = "broken"1492 state = "broken"
M internal/app/run_test.go +13 -13
@@ -588,26 +588,26 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
588588 func TestListShowsBroken(t *testing.T) {
589589 var out bytes.Buffer
590590 printSessions(&out, []ports.SessionInfo{
591- {Name: "fresh", State: ports.SessionStopped},
592- {Name: "loading", State: ports.SessionStopped},
591+ {Name: "fresh", State: ports.SessionDown},
592+ {Name: "loading", State: ports.SessionDown},
593593 {Name: "broken", State: ports.SessionBroken},
594594 })
595595 require.Contains(t, out.String(), "fresh")
596- require.Contains(t, out.String(), "stopped")
596+ require.Contains(t, out.String(), "down")
597597 require.Contains(t, out.String(), "loading")
598598 require.Contains(t, out.String(), "broken")
599599 require.NotContains(t, out.String(), "restoring")
600600 require.NotContains(t, out.String(), "degraded")
601601 }
602602
603-func TestPrintSessionsShowsStoppedState(t *testing.T) {
603+func TestPrintSessionsShowsDownState(t *testing.T) {
604604 var out bytes.Buffer
605605 printSessions(&out, []ports.SessionInfo{
606- {Name: "main", State: ports.SessionRunning, Tabs: 2, Attached: true},
607- {Name: "old", State: ports.SessionStopped},
606+ {Name: "main", State: ports.SessionUp, Tabs: 2, Attached: true},
607+ {Name: "old", State: ports.SessionDown},
608608 })
609609 got := out.String()
610- for _, want := range []string{"NAME", "STATE", "main", "running", "2", "yes", "old", "stopped", "-"} {
610+ for _, want := range []string{"NAME", "STATE", "main", "up", "2", "yes", "old", "down", "-"} {
611611 if !strings.Contains(got, want) {
612612 t.Fatalf("printSessions output %q missing %q", got, want)
613613 }
@@ -617,12 +617,12 @@ func TestPrintSessionsShowsStoppedState(t *testing.T) {
617617 func TestPrintSessionsMarksEphemeral(t *testing.T) {
618618 var buf bytes.Buffer
619619 printSessions(&buf, []ports.SessionInfo{
620- {Name: "0", State: ports.SessionRunning, Ephemeral: true, Tabs: 1, Attached: false},
621- {Name: "work", State: ports.SessionRunning, Tabs: 2, Attached: true},
622- {Name: "old", State: ports.SessionStopped},
620+ {Name: "0", State: ports.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
621+ {Name: "work", State: ports.SessionUp, Tabs: 2, Attached: true},
622+ {Name: "old", State: ports.SessionDown},
623623 })
624624 out := buf.String()
625- for _, want := range []string{"0", "temporary", "work", "running", "old", "stopped"} {
625+ for _, want := range []string{"0", "temporary", "work", "up", "old", "down"} {
626626 require.Contains(t, out, want)
627627 }
628628 }
@@ -634,7 +634,7 @@ func newTestPersister(t *testing.T, stateDir string) *persist.Persister {
634634 return persist.New(store)
635635 }
636636
637-func TestRunListReadsStoppedSessionsWithoutDaemon(t *testing.T) {
637+func TestRunListReadsDownSessionsWithoutDaemon(t *testing.T) {
638638 stateRoot, runtimeRoot := t.TempDir(), t.TempDir()
639639 t.Setenv("XDG_STATE_HOME", stateRoot)
640640 t.Setenv("XDG_RUNTIME_DIR", runtimeRoot)
@@ -653,7 +653,7 @@ func TestRunListReadsStoppedSessionsWithoutDaemon(t *testing.T) {
653653 t.Fatalf("runList error = %v", err)
654654 }
655655 })
656- for _, want := range []string{"stored", "stopped", "-"} {
656+ for _, want := range []string{"stored", "down", "-"} {
657657 if !strings.Contains(got, want) {
658658 t.Fatalf("runList output %q missing %q", got, want)
659659 }
@@ -588,26 +588,26 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
588 func TestListShowsBroken(t *testing.T) {588 func TestListShowsBroken(t *testing.T) {
589 var out bytes.Buffer589 var out bytes.Buffer
590 printSessions(&out, []ports.SessionInfo{590 printSessions(&out, []ports.SessionInfo{
591- {Name: "fresh", State: ports.SessionStopped},591+ {Name: "fresh", State: ports.SessionDown},
592- {Name: "loading", State: ports.SessionStopped},592+ {Name: "loading", State: ports.SessionDown},
593 {Name: "broken", State: ports.SessionBroken},593 {Name: "broken", State: ports.SessionBroken},
594 })594 })
595 require.Contains(t, out.String(), "fresh")595 require.Contains(t, out.String(), "fresh")
596- require.Contains(t, out.String(), "stopped")596+ require.Contains(t, out.String(), "down")
597 require.Contains(t, out.String(), "loading")597 require.Contains(t, out.String(), "loading")
598 require.Contains(t, out.String(), "broken")598 require.Contains(t, out.String(), "broken")
599 require.NotContains(t, out.String(), "restoring")599 require.NotContains(t, out.String(), "restoring")
600 require.NotContains(t, out.String(), "degraded")600 require.NotContains(t, out.String(), "degraded")
601 }601 }
602 602
603-func TestPrintSessionsShowsStoppedState(t *testing.T) {603+func TestPrintSessionsShowsDownState(t *testing.T) {
604 var out bytes.Buffer604 var out bytes.Buffer
605 printSessions(&out, []ports.SessionInfo{605 printSessions(&out, []ports.SessionInfo{
606- {Name: "main", State: ports.SessionRunning, Tabs: 2, Attached: true},606+ {Name: "main", State: ports.SessionUp, Tabs: 2, Attached: true},
607- {Name: "old", State: ports.SessionStopped},607+ {Name: "old", State: ports.SessionDown},
608 })608 })
609 got := out.String()609 got := out.String()
610- for _, want := range []string{"NAME", "STATE", "main", "running", "2", "yes", "old", "stopped", "-"} {610+ for _, want := range []string{"NAME", "STATE", "main", "up", "2", "yes", "old", "down", "-"} {
611 if !strings.Contains(got, want) {611 if !strings.Contains(got, want) {
612 t.Fatalf("printSessions output %q missing %q", got, want)612 t.Fatalf("printSessions output %q missing %q", got, want)
613 }613 }
@@ -617,12 +617,12 @@ func TestPrintSessionsShowsStoppedState(t *testing.T) {
617 func TestPrintSessionsMarksEphemeral(t *testing.T) {617 func TestPrintSessionsMarksEphemeral(t *testing.T) {
618 var buf bytes.Buffer618 var buf bytes.Buffer
619 printSessions(&buf, []ports.SessionInfo{619 printSessions(&buf, []ports.SessionInfo{
620- {Name: "0", State: ports.SessionRunning, Ephemeral: true, Tabs: 1, Attached: false},620+ {Name: "0", State: ports.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
621- {Name: "work", State: ports.SessionRunning, Tabs: 2, Attached: true},621+ {Name: "work", State: ports.SessionUp, Tabs: 2, Attached: true},
622- {Name: "old", State: ports.SessionStopped},622+ {Name: "old", State: ports.SessionDown},
623 })623 })
624 out := buf.String()624 out := buf.String()
625- for _, want := range []string{"0", "temporary", "work", "running", "old", "stopped"} {625+ for _, want := range []string{"0", "temporary", "work", "up", "old", "down"} {
626 require.Contains(t, out, want)626 require.Contains(t, out, want)
627 }627 }
628 }628 }
@@ -634,7 +634,7 @@ func newTestPersister(t *testing.T, stateDir string) *persist.Persister {
634 return persist.New(store)634 return persist.New(store)
635 }635 }
636 636
637-func TestRunListReadsStoppedSessionsWithoutDaemon(t *testing.T) {637+func TestRunListReadsDownSessionsWithoutDaemon(t *testing.T) {
638 stateRoot, runtimeRoot := t.TempDir(), t.TempDir()638 stateRoot, runtimeRoot := t.TempDir(), t.TempDir()
639 t.Setenv("XDG_STATE_HOME", stateRoot)639 t.Setenv("XDG_STATE_HOME", stateRoot)
640 t.Setenv("XDG_RUNTIME_DIR", runtimeRoot)640 t.Setenv("XDG_RUNTIME_DIR", runtimeRoot)
@@ -653,7 +653,7 @@ func TestRunListReadsStoppedSessionsWithoutDaemon(t *testing.T) {
653 t.Fatalf("runList error = %v", err)653 t.Fatalf("runList error = %v", err)
654 }654 }
655 })655 })
656- for _, want := range []string{"stored", "stopped", "-"} {656+ for _, want := range []string{"stored", "down", "-"} {
657 if !strings.Contains(got, want) {657 if !strings.Contains(got, want) {
658 t.Fatalf("runList output %q missing %q", got, want)658 t.Fatalf("runList output %q missing %q", got, want)
659 }659 }
M internal/domain/lifecycle.go +3 -3
@@ -138,15 +138,15 @@ func (t RemoteSessionTarget) Validate() error {
138138 }
139139 if t.Stopped {
140140 if t.LiveTabID != "" || t.StoppedTab.Validate() != nil {
141- return errors.New("stopped remote target requires an ordinal or stable tab selector")
141+ return errors.New("down remote target requires an ordinal or stable tab selector")
142142 }
143143 return nil
144144 }
145145 if err := ValidateTabStableID(t.LiveTabID); err != nil {
146- return errors.New("running remote target requires a stable tab ID")
146+ return errors.New("up remote target requires a stable tab ID")
147147 }
148148 if t.StoppedTab != (TabSelector{}) {
149- return errors.New("running remote target cannot carry a stopped selector")
149+ return errors.New("up remote target cannot carry a down selector")
150150 }
151151 return nil
152152 }
@@ -138,15 +138,15 @@ func (t RemoteSessionTarget) Validate() error {
138 }138 }
139 if t.Stopped {139 if t.Stopped {
140 if t.LiveTabID != "" || t.StoppedTab.Validate() != nil {140 if t.LiveTabID != "" || t.StoppedTab.Validate() != nil {
141- return errors.New("stopped remote target requires an ordinal or stable tab selector")141+ return errors.New("down remote target requires an ordinal or stable tab selector")
142 }142 }
143 return nil143 return nil
144 }144 }
145 if err := ValidateTabStableID(t.LiveTabID); err != nil {145 if err := ValidateTabStableID(t.LiveTabID); err != nil {
146- return errors.New("running remote target requires a stable tab ID")146+ return errors.New("up remote target requires a stable tab ID")
147 }147 }
148 if t.StoppedTab != (TabSelector{}) {148 if t.StoppedTab != (TabSelector{}) {
149- return errors.New("running remote target cannot carry a stopped selector")149+ return errors.New("up remote target cannot carry a down selector")
150 }150 }
151 return nil151 return nil
152 }152 }
M internal/ports/remote.go +9 -9
@@ -64,7 +64,7 @@ type RemotePreviewClient interface {
6464 const (
6565 // RemoteCatalogSchemaVersion is independent from the IPC protocol. A
6666 // catalogue can be rejected without changing the attachment wire layout.
67- RemoteCatalogSchemaVersion uint16 = 1
67+ RemoteCatalogSchemaVersion uint16 = 2
6868
6969 RemoteCatalogMaxHosts = 64
7070 RemoteCatalogMaxSessions = 256
@@ -103,7 +103,7 @@ type RemoteCatalogTab struct {
103103 }
104104
105105 // RemoteCatalogSession is one session in the remote discovery catalog. State
106-// is an explicit string contract (running|stopped|broken), not SessionState.
106+// is an explicit string contract (up|down|broken), not SessionState.
107107 //
108108 // Tabs is interface-typed only to keep old v0.x callers that supplied a count
109109 // source-compatible while the versioned schema carries []RemoteCatalogTab.
@@ -301,7 +301,7 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
301301 if session.ActiveTabID != "" {
302302 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)
303303 }
304- if session.State == "running" && session.Tabs == nil {
304+ if session.State == "up" && session.Tabs == nil {
305305 return fmt.Errorf("%w: missing tab list", ErrInvalidRemoteCatalog)
306306 }
307307 }
@@ -320,8 +320,8 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
320320 if tab.Index != uint16(i) {
321321 return fmt.Errorf("%w: tab indexes are not ordered", ErrInvalidRemoteCatalog)
322322 }
323- if session.State == "running" && tab.ID == "" {
324- return fmt.Errorf("%w: running tab has zero ID", ErrInvalidRemoteCatalog)
323+ if session.State == "up" && tab.ID == "" {
324+ return fmt.Errorf("%w: up tab has zero ID", ErrInvalidRemoteCatalog)
325325 }
326326 if tab.ID != "" {
327327 if err := domain.ValidateTabStableID(domain.TabStableID(tab.ID)); err != nil {
@@ -335,8 +335,8 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
335335 bytes += len(tab.ID) + len(tab.Name) + len(tab.Detail)
336336 }
337337 if session.ActiveTabID != "" {
338- if session.State != "running" {
339- return fmt.Errorf("%w: stopped or broken session has an active tab", ErrInvalidRemoteCatalog)
338+ if session.State != "up" {
339+ return fmt.Errorf("%w: down or broken session has an active tab", ErrInvalidRemoteCatalog)
340340 }
341341 if _, ok := tabIDs[session.ActiveTabID]; !ok {
342342 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)
@@ -404,7 +404,7 @@ func validRemoteCatalogText(value string) bool {
404404
405405 func validRemoteCatalogState(state string) bool {
406406 switch state {
407- case "running", "stopped", "broken":
407+ case "up", "down", "broken":
408408 return true
409409 default:
410410 return false
@@ -413,7 +413,7 @@ func validRemoteCatalogState(state string) bool {
413413
414414 func validRemoteCatalogReason(reason string) bool {
415415 switch reason {
416- case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_stopped", "session_broken", "identity_changed", "not_found", "timeout", "malformed":
416+ case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_down", "session_broken", "identity_changed", "not_found", "timeout", "malformed":
417417 return true
418418 default:
419419 return false
@@ -64,7 +64,7 @@ type RemotePreviewClient interface {
64 const (64 const (
65 // RemoteCatalogSchemaVersion is independent from the IPC protocol. A65 // RemoteCatalogSchemaVersion is independent from the IPC protocol. A
66 // catalogue can be rejected without changing the attachment wire layout.66 // catalogue can be rejected without changing the attachment wire layout.
67- RemoteCatalogSchemaVersion uint16 = 167+ RemoteCatalogSchemaVersion uint16 = 2
68 68
69 RemoteCatalogMaxHosts = 6469 RemoteCatalogMaxHosts = 64
70 RemoteCatalogMaxSessions = 25670 RemoteCatalogMaxSessions = 256
@@ -103,7 +103,7 @@ type RemoteCatalogTab struct {
103 }103 }
104 104
105 // RemoteCatalogSession is one session in the remote discovery catalog. State105 // RemoteCatalogSession is one session in the remote discovery catalog. State
106-// is an explicit string contract (running|stopped|broken), not SessionState.106+// is an explicit string contract (up|down|broken), not SessionState.
107 //107 //
108 // Tabs is interface-typed only to keep old v0.x callers that supplied a count108 // Tabs is interface-typed only to keep old v0.x callers that supplied a count
109 // source-compatible while the versioned schema carries []RemoteCatalogTab.109 // source-compatible while the versioned schema carries []RemoteCatalogTab.
@@ -301,7 +301,7 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
301 if session.ActiveTabID != "" {301 if session.ActiveTabID != "" {
302 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)302 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)
303 }303 }
304- if session.State == "running" && session.Tabs == nil {304+ if session.State == "up" && session.Tabs == nil {
305 return fmt.Errorf("%w: missing tab list", ErrInvalidRemoteCatalog)305 return fmt.Errorf("%w: missing tab list", ErrInvalidRemoteCatalog)
306 }306 }
307 }307 }
@@ -320,8 +320,8 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
320 if tab.Index != uint16(i) {320 if tab.Index != uint16(i) {
321 return fmt.Errorf("%w: tab indexes are not ordered", ErrInvalidRemoteCatalog)321 return fmt.Errorf("%w: tab indexes are not ordered", ErrInvalidRemoteCatalog)
322 }322 }
323- if session.State == "running" && tab.ID == "" {323+ if session.State == "up" && tab.ID == "" {
324- return fmt.Errorf("%w: running tab has zero ID", ErrInvalidRemoteCatalog)324+ return fmt.Errorf("%w: up tab has zero ID", ErrInvalidRemoteCatalog)
325 }325 }
326 if tab.ID != "" {326 if tab.ID != "" {
327 if err := domain.ValidateTabStableID(domain.TabStableID(tab.ID)); err != nil {327 if err := domain.ValidateTabStableID(domain.TabStableID(tab.ID)); err != nil {
@@ -335,8 +335,8 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
335 bytes += len(tab.ID) + len(tab.Name) + len(tab.Detail)335 bytes += len(tab.ID) + len(tab.Name) + len(tab.Detail)
336 }336 }
337 if session.ActiveTabID != "" {337 if session.ActiveTabID != "" {
338- if session.State != "running" {338+ if session.State != "up" {
339- return fmt.Errorf("%w: stopped or broken session has an active tab", ErrInvalidRemoteCatalog)339+ return fmt.Errorf("%w: down or broken session has an active tab", ErrInvalidRemoteCatalog)
340 }340 }
341 if _, ok := tabIDs[session.ActiveTabID]; !ok {341 if _, ok := tabIDs[session.ActiveTabID]; !ok {
342 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)342 return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog)
@@ -404,7 +404,7 @@ func validRemoteCatalogText(value string) bool {
404 404
405 func validRemoteCatalogState(state string) bool {405 func validRemoteCatalogState(state string) bool {
406 switch state {406 switch state {
407- case "running", "stopped", "broken":407+ case "up", "down", "broken":
408 return true408 return true
409 default:409 default:
410 return false410 return false
@@ -413,7 +413,7 @@ func validRemoteCatalogState(state string) bool {
413 413
414 func validRemoteCatalogReason(reason string) bool {414 func validRemoteCatalogReason(reason string) bool {
415 switch reason {415 switch reason {
416- case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_stopped", "session_broken", "identity_changed", "not_found", "timeout", "malformed":416+ case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_down", "session_broken", "identity_changed", "not_found", "timeout", "malformed":
417 return true417 return true
418 default:418 default:
419 return false419 return false
A internal/ports/remote_state_test.go +27 -0
@@ -0,0 +1,27 @@
1+package ports
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestRemoteCatalogStateContract(t *testing.T) {
10+ require.Equal(t, uint16(2), RemoteCatalogSchemaVersion)
11+
12+ for _, state := range []string{"up", "down", "broken"} {
13+ t.Run(state, func(t *testing.T) {
14+ err := ValidateRemoteCatalog(RemoteCatalog{
15+ ProtocolVersion: ProtocolVersion,
16+ Sessions: []RemoteCatalogSession{{Name: "work", State: state, Tabs: 1}},
17+ })
18+ require.NoError(t, err)
19+ })
20+ }
21+
22+ err := ValidateRemoteCatalog(RemoteCatalog{
23+ ProtocolVersion: ProtocolVersion,
24+ Sessions: []RemoteCatalogSession{{Name: "work", State: "running", Tabs: 1}},
25+ })
26+ require.ErrorIs(t, err, ErrRemoteCatalogUnknownState)
27+}
@@ -0,0 +1,27 @@
1+package ports
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestRemoteCatalogStateContract(t *testing.T) {
10+ require.Equal(t, uint16(2), RemoteCatalogSchemaVersion)
11+
12+ for _, state := range []string{"up", "down", "broken"} {
13+ t.Run(state, func(t *testing.T) {
14+ err := ValidateRemoteCatalog(RemoteCatalog{
15+ ProtocolVersion: ProtocolVersion,
16+ Sessions: []RemoteCatalogSession{{Name: "work", State: state, Tabs: 1}},
17+ })
18+ require.NoError(t, err)
19+ })
20+ }
21+
22+ err := ValidateRemoteCatalog(RemoteCatalog{
23+ ProtocolVersion: ProtocolVersion,
24+ Sessions: []RemoteCatalogSession{{Name: "work", State: "running", Tabs: 1}},
25+ })
26+ require.ErrorIs(t, err, ErrRemoteCatalogUnknownState)
27+}
M internal/ports/wire.go +2 -2
@@ -288,8 +288,8 @@ type Kill struct {
288288 type SessionState uint8
289289
290290 const (
291- SessionRunning SessionState = iota
292- SessionStopped
291+ SessionUp SessionState = iota
292+ SessionDown
293293 SessionBroken
294294 )
295295
@@ -288,8 +288,8 @@ type Kill struct {
288 type SessionState uint8288 type SessionState uint8
289 289
290 const (290 const (
291- SessionRunning SessionState = iota291+ SessionUp SessionState = iota
292- SessionStopped292+ SessionDown
293 SessionBroken293 SessionBroken
294 )294 )
295 295
M internal/ports/wire_final_test.go +2 -2
@@ -237,7 +237,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
237237 {
238238 name: "sessions ephemeral boolean",
239239 payload: func() []byte {
240- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}})
240+ b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
241241 b[6] = 2
242242 return b
243243 }(),
@@ -246,7 +246,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
246246 {
247247 name: "sessions attached boolean",
248248 payload: func() []byte {
249- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}})
249+ b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
250250 b[9] = 2
251251 return b
252252 }(),
@@ -237,7 +237,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
237 {237 {
238 name: "sessions ephemeral boolean",238 name: "sessions ephemeral boolean",
239 payload: func() []byte {239 payload: func() []byte {
240- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}})240+ b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
241 b[6] = 2241 b[6] = 2
242 return b242 return b
243 }(),243 }(),
@@ -246,7 +246,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
246 {246 {
247 name: "sessions attached boolean",247 name: "sessions attached boolean",
248 payload: func() []byte {248 payload: func() []byte {
249- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}})249+ b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
250 b[9] = 2250 b[9] = 2
251 return b251 return b
252 }(),252 }(),
M internal/ports/wire_test.go +4 -4
@@ -776,8 +776,8 @@ func TestSessionInfoRecoveryState(t *testing.T) {
776776 state SessionState
777777 want []byte
778778 }{
779- {name: "running", state: SessionRunning, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
780- {name: "stopped", state: SessionStopped, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
779+ {name: "up", state: SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
780+ {name: "down", state: SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
781781 {name: "broken", state: SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
782782 }
783783 for _, tt := range tests {
@@ -813,8 +813,8 @@ func TestSessionsGoldenAndRoundTrip(t *testing.T) {
813813 {
814814 name: "two",
815815 msg: Sessions{Sessions: []SessionInfo{
816- {SessionID: "0", Name: "0", State: SessionRunning, Ephemeral: true, Tabs: 1, Attached: false},
817- {SessionID: "work", Name: "proj", State: SessionStopped, Ephemeral: false, Tabs: 5, Attached: true},
816+ {SessionID: "0", Name: "0", State: SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
817+ {SessionID: "work", Name: "proj", State: SessionDown, Ephemeral: false, Tabs: 5, Attached: true},
818818 }},
819819 want: []byte{
820820 0x00, 0x02,
@@ -776,8 +776,8 @@ func TestSessionInfoRecoveryState(t *testing.T) {
776 state SessionState776 state SessionState
777 want []byte777 want []byte
778 }{778 }{
779- {name: "running", state: SessionRunning, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},779+ {name: "up", state: SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
780- {name: "stopped", state: SessionStopped, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},780+ {name: "down", state: SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
781 {name: "broken", state: SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},781 {name: "broken", state: SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
782 }782 }
783 for _, tt := range tests {783 for _, tt := range tests {
@@ -813,8 +813,8 @@ func TestSessionsGoldenAndRoundTrip(t *testing.T) {
813 {813 {
814 name: "two",814 name: "two",
815 msg: Sessions{Sessions: []SessionInfo{815 msg: Sessions{Sessions: []SessionInfo{
816- {SessionID: "0", Name: "0", State: SessionRunning, Ephemeral: true, Tabs: 1, Attached: false},816+ {SessionID: "0", Name: "0", State: SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
817- {SessionID: "work", Name: "proj", State: SessionStopped, Ephemeral: false, Tabs: 5, Attached: true},817+ {SessionID: "work", Name: "proj", State: SessionDown, Ephemeral: false, Tabs: 5, Attached: true},
818 }},818 }},
819 want: []byte{819 want: []byte{
820 0x00, 0x02,820 0x00, 0x02,
M internal/usecase/daemon/coderabbit_regression_test.go +4 -4
@@ -120,7 +120,7 @@ func TestRestoreFailureCannotOverwriteDiscardReplacement(t *testing.T) {
120120 entry := d.stopped[record.Name]
121121 d.mu.Unlock()
122122 require.Equal(t, persisted, entry.record)
123- require.Equal(t, ports.SessionStopped, entry.state)
123+ require.Equal(t, ports.SessionDown, entry.state)
124124 }
125125
126126 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
@@ -138,7 +138,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
138138 record.Committed = &ref
139139 return record
140140 },
141- state: ports.SessionStopped,
141+ state: ports.SessionDown,
142142 },
143143 {
144144 name: "newer incarnation",
@@ -146,7 +146,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
146146 record.IncarnationID = domain.IncarnationID{9}
147147 return record
148148 },
149- state: ports.SessionStopped,
149+ state: ports.SessionDown,
150150 },
151151 {
152152 name: "degraded authority",
@@ -223,7 +223,7 @@ func TestCreateSessionRechecksShutdownAfterCatalogueRead(t *testing.T) {
223223 d := newTestDaemon(t, nil, stubClock{})
224224 d.catalogue = catalogue
225225 d.persistEnabled = true
226- d.stopped[record.Name] = stoppedSessionFromRecord(record, ports.SessionStopped, make(chan struct{}))
226+ d.stopped[record.Name] = stoppedSessionFromRecord(record, ports.SessionDown, make(chan struct{}))
227227
228228 result := make(chan error, 1)
229229 go func() {
@@ -120,7 +120,7 @@ func TestRestoreFailureCannotOverwriteDiscardReplacement(t *testing.T) {
120 entry := d.stopped[record.Name]120 entry := d.stopped[record.Name]
121 d.mu.Unlock()121 d.mu.Unlock()
122 require.Equal(t, persisted, entry.record)122 require.Equal(t, persisted, entry.record)
123- require.Equal(t, ports.SessionStopped, entry.state)123+ require.Equal(t, ports.SessionDown, entry.state)
124 }124 }
125 125
126 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {126 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
@@ -138,7 +138,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
138 record.Committed = &ref138 record.Committed = &ref
139 return record139 return record
140 },140 },
141- state: ports.SessionStopped,141+ state: ports.SessionDown,
142 },142 },
143 {143 {
144 name: "newer incarnation",144 name: "newer incarnation",
@@ -146,7 +146,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
146 record.IncarnationID = domain.IncarnationID{9}146 record.IncarnationID = domain.IncarnationID{9}
147 return record147 return record
148 },148 },
149- state: ports.SessionStopped,149+ state: ports.SessionDown,
150 },150 },
151 {151 {
152 name: "degraded authority",152 name: "degraded authority",
@@ -223,7 +223,7 @@ func TestCreateSessionRechecksShutdownAfterCatalogueRead(t *testing.T) {
223 d := newTestDaemon(t, nil, stubClock{})223 d := newTestDaemon(t, nil, stubClock{})
224 d.catalogue = catalogue224 d.catalogue = catalogue
225 d.persistEnabled = true225 d.persistEnabled = true
226- d.stopped[record.Name] = stoppedSessionFromRecord(record, ports.SessionStopped, make(chan struct{}))226+ d.stopped[record.Name] = stoppedSessionFromRecord(record, ports.SessionDown, make(chan struct{}))
227 227
228 result := make(chan error, 1)228 result := make(chan error, 1)
229 go func() {229 go func() {
M internal/usecase/daemon/control.go +4 -4
@@ -643,7 +643,7 @@ func (e controlExec) SessionRecovery(action string) (string, error) {
643643 return "", err
644644 }
645645 if ok {
646- e.d.setStoppedRecovery(record, ports.SessionStopped)
646+ e.d.setStoppedRecovery(record, ports.SessionDown)
647647 }
648648 return "", nil
649649 }
@@ -736,7 +736,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
736736 row := ports.RemoteCatalogSession{
737737 LifecycleID: snap.incarnation,
738738 Name: snap.name,
739- State: "running",
739+ State: "up",
740740 Ephemeral: snap.ephemeral,
741741 Tabs: tabs,
742742 Attached: snap.attached,
@@ -752,7 +752,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
752752 if err != nil {
753753 return "", err
754754 }
755- state := "stopped"
755+ state := "down"
756756 reason := ""
757757 if entry.state == ports.SessionBroken || entry.record.DegradedReason != "" {
758758 state = "broken"
@@ -791,7 +791,7 @@ func (e controlExec) remoteCatalogLegacy(sessions []*session) (string, error) {
791791 snap := sess.snapshotView(viewOptions{})
792792 rows = append(rows, ports.RemoteCatalogSession{
793793 Name: snap.name,
794- State: "running",
794+ State: "up",
795795 Ephemeral: snap.ephemeral,
796796 Tabs: ports.SaturateUint16(snap.tabCount),
797797 Attached: snap.attached,
@@ -643,7 +643,7 @@ func (e controlExec) SessionRecovery(action string) (string, error) {
643 return "", err643 return "", err
644 }644 }
645 if ok {645 if ok {
646- e.d.setStoppedRecovery(record, ports.SessionStopped)646+ e.d.setStoppedRecovery(record, ports.SessionDown)
647 }647 }
648 return "", nil648 return "", nil
649 }649 }
@@ -736,7 +736,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
736 row := ports.RemoteCatalogSession{736 row := ports.RemoteCatalogSession{
737 LifecycleID: snap.incarnation,737 LifecycleID: snap.incarnation,
738 Name: snap.name,738 Name: snap.name,
739- State: "running",739+ State: "up",
740 Ephemeral: snap.ephemeral,740 Ephemeral: snap.ephemeral,
741 Tabs: tabs,741 Tabs: tabs,
742 Attached: snap.attached,742 Attached: snap.attached,
@@ -752,7 +752,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
752 if err != nil {752 if err != nil {
753 return "", err753 return "", err
754 }754 }
755- state := "stopped"755+ state := "down"
756 reason := ""756 reason := ""
757 if entry.state == ports.SessionBroken || entry.record.DegradedReason != "" {757 if entry.state == ports.SessionBroken || entry.record.DegradedReason != "" {
758 state = "broken"758 state = "broken"
@@ -791,7 +791,7 @@ func (e controlExec) remoteCatalogLegacy(sessions []*session) (string, error) {
791 snap := sess.snapshotView(viewOptions{})791 snap := sess.snapshotView(viewOptions{})
792 rows = append(rows, ports.RemoteCatalogSession{792 rows = append(rows, ports.RemoteCatalogSession{
793 Name: snap.name,793 Name: snap.name,
794- State: "running",794+ State: "up",
795 Ephemeral: snap.ephemeral,795 Ephemeral: snap.ephemeral,
796 Tabs: ports.SaturateUint16(snap.tabCount),796 Tabs: ports.SaturateUint16(snap.tabCount),
797 Attached: snap.attached,797 Attached: snap.attached,
M internal/usecase/daemon/control_test.go +5 -5
@@ -735,7 +735,7 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
735735 build.mruAt.Store(1)
736736
737737 d.mu.Lock()
738- d.stopped["old"] = stoppedSession{name: "old", cwd: "/tmp/old", createdAt: 1, state: ports.SessionStopped}
738+ d.stopped["old"] = stoppedSession{name: "old", cwd: "/tmp/old", createdAt: 1, state: ports.SessionDown}
739739 d.mu.Unlock()
740740
741741 listBefore := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})
@@ -765,12 +765,12 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
765765 require.True(t, hasProtocol && hasSessions)
766766
767767 require.Equal(t, []ports.RemoteCatalogSession{
768- {Name: "build", State: "running", Ephemeral: true, Tabs: 1, Attached: false},
769- {Name: "work", State: "running", Ephemeral: false, Tabs: 2, Attached: true},
768+ {Name: "build", State: "up", Ephemeral: true, Tabs: 1, Attached: false},
769+ {Name: "work", State: "up", Ephemeral: false, Tabs: 2, Attached: true},
770770 }, catalog.Sessions)
771771
772772 for _, session := range catalog.Sessions {
773- require.Equal(t, "running", session.State)
773+ require.Equal(t, "up", session.State)
774774 require.NotEqual(t, "old", session.Name)
775775 }
776776
@@ -797,7 +797,7 @@ func TestRemoteCatalogTabCountSaturates(t *testing.T) {
797797 var catalog ports.RemoteCatalog
798798 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
799799 require.Equal(t, []ports.RemoteCatalogSession{{
800- Name: "work", State: "running", Ephemeral: true, Tabs: int(^uint16(0)), Attached: false,
800+ Name: "work", State: "up", Ephemeral: true, Tabs: int(^uint16(0)), Attached: false,
801801 }}, catalog.Sessions)
802802 }
803803
@@ -735,7 +735,7 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
735 build.mruAt.Store(1)735 build.mruAt.Store(1)
736 736
737 d.mu.Lock()737 d.mu.Lock()
738- d.stopped["old"] = stoppedSession{name: "old", cwd: "/tmp/old", createdAt: 1, state: ports.SessionStopped}738+ d.stopped["old"] = stoppedSession{name: "old", cwd: "/tmp/old", createdAt: 1, state: ports.SessionDown}
739 d.mu.Unlock()739 d.mu.Unlock()
740 740
741 listBefore := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})741 listBefore := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})
@@ -765,12 +765,12 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
765 require.True(t, hasProtocol && hasSessions)765 require.True(t, hasProtocol && hasSessions)
766 766
767 require.Equal(t, []ports.RemoteCatalogSession{767 require.Equal(t, []ports.RemoteCatalogSession{
768- {Name: "build", State: "running", Ephemeral: true, Tabs: 1, Attached: false},768+ {Name: "build", State: "up", Ephemeral: true, Tabs: 1, Attached: false},
769- {Name: "work", State: "running", Ephemeral: false, Tabs: 2, Attached: true},769+ {Name: "work", State: "up", Ephemeral: false, Tabs: 2, Attached: true},
770 }, catalog.Sessions)770 }, catalog.Sessions)
771 771
772 for _, session := range catalog.Sessions {772 for _, session := range catalog.Sessions {
773- require.Equal(t, "running", session.State)773+ require.Equal(t, "up", session.State)
774 require.NotEqual(t, "old", session.Name)774 require.NotEqual(t, "old", session.Name)
775 }775 }
776 776
@@ -797,7 +797,7 @@ func TestRemoteCatalogTabCountSaturates(t *testing.T) {
797 var catalog ports.RemoteCatalog797 var catalog ports.RemoteCatalog
798 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))798 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
799 require.Equal(t, []ports.RemoteCatalogSession{{799 require.Equal(t, []ports.RemoteCatalogSession{{
800- Name: "work", State: "running", Ephemeral: true, Tabs: int(^uint16(0)), Attached: false,800+ Name: "work", State: "up", Ephemeral: true, Tabs: int(^uint16(0)), Attached: false,
801 }}, catalog.Sessions)801 }}, catalog.Sessions)
802 }802 }
803 803
M internal/usecase/daemon/daemon.go +3 -3
@@ -671,7 +671,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
671671 state, done := initialSessionState(r)
672672 d.stopped[r.Name] = stoppedSessionFromRecord(r, state, done)
673673 } else {
674- d.stopped[r.Name] = stoppedSessionFromRecord(r, ports.SessionStopped, nil)
674+ d.stopped[r.Name] = stoppedSessionFromRecord(r, ports.SessionDown, nil)
675675 }
676676 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {
677677 maxCreatedAt = r.CreatedAt
@@ -1083,7 +1083,7 @@ func (d *Daemon) handleList(tr ports.Transport) {
10831083 info := ports.SessionInfo{
10841084 SessionID: string(s.id),
10851085 Name: s.name,
1086- State: ports.SessionRunning,
1086+ State: ports.SessionUp,
10871087 Ephemeral: s.ephemeral,
10881088 Tabs: uint16(len(s.tabs)),
10891089 Attached: len(s.attachments) != 0,
@@ -1096,7 +1096,7 @@ func (d *Daemon) handleList(tr ports.Transport) {
10961096 if _, live := liveNames[name]; live {
10971097 continue
10981098 }
1099- state := ports.SessionStopped
1099+ state := ports.SessionDown
11001100 if stopped.purging || stopped.state == ports.SessionBroken {
11011101 // Purge is the dominant externally visible state: restoration must
11021102 // never make a deletion-reserved record appear attachable.
@@ -671,7 +671,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
671 state, done := initialSessionState(r)671 state, done := initialSessionState(r)
672 d.stopped[r.Name] = stoppedSessionFromRecord(r, state, done)672 d.stopped[r.Name] = stoppedSessionFromRecord(r, state, done)
673 } else {673 } else {
674- d.stopped[r.Name] = stoppedSessionFromRecord(r, ports.SessionStopped, nil)674+ d.stopped[r.Name] = stoppedSessionFromRecord(r, ports.SessionDown, nil)
675 }675 }
676 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {676 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {
677 maxCreatedAt = r.CreatedAt677 maxCreatedAt = r.CreatedAt
@@ -1083,7 +1083,7 @@ func (d *Daemon) handleList(tr ports.Transport) {
1083 info := ports.SessionInfo{1083 info := ports.SessionInfo{
1084 SessionID: string(s.id),1084 SessionID: string(s.id),
1085 Name: s.name,1085 Name: s.name,
1086- State: ports.SessionRunning,1086+ State: ports.SessionUp,
1087 Ephemeral: s.ephemeral,1087 Ephemeral: s.ephemeral,
1088 Tabs: uint16(len(s.tabs)),1088 Tabs: uint16(len(s.tabs)),
1089 Attached: len(s.attachments) != 0,1089 Attached: len(s.attachments) != 0,
@@ -1096,7 +1096,7 @@ func (d *Daemon) handleList(tr ports.Transport) {
1096 if _, live := liveNames[name]; live {1096 if _, live := liveNames[name]; live {
1097 continue1097 continue
1098 }1098 }
1099- state := ports.SessionStopped1099+ state := ports.SessionDown
1100 if stopped.purging || stopped.state == ports.SessionBroken {1100 if stopped.purging || stopped.state == ports.SessionBroken {
1101 // Purge is the dominant externally visible state: restoration must1101 // Purge is the dominant externally visible state: restoration must
1102 // never make a deletion-reserved record appear attachable.1102 // never make a deletion-reserved record appear attachable.
M internal/usecase/daemon/durable_recovery_test.go +13 -13
@@ -304,7 +304,7 @@ func TestCatalogueRestoreIndependent(t *testing.T) {
304304 healthyEntry := d.stopped[healthy.Name]
305305 brokenEntry := d.stopped[broken.Name]
306306 d.mu.Unlock()
307- require.Equal(t, ports.SessionStopped, healthyEntry.state)
307+ require.Equal(t, ports.SessionDown, healthyEntry.state)
308308 require.Equal(t, ports.SessionBroken, brokenEntry.state)
309309 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {
310310 select {
@@ -344,14 +344,14 @@ func TestCatalogueRestoreResetsOnlyIncompatibleCheckpoint(t *testing.T) {
344344 require.Empty(t, fresh.DegradedReason)
345345 require.Empty(t, fresh.TabNames)
346346 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)
347- require.Equal(t, ports.SessionStopped, d.stopped[incompatible.Name].state)
347+ require.Equal(t, ports.SessionDown, d.stopped[incompatible.Name].state)
348348 require.Equal(t, fresh, d.stopped[incompatible.Name].record)
349349
350350 restoredSibling, ok, err := catalogue.Record(healthy.Name)
351351 require.NoError(t, err)
352352 require.True(t, ok)
353353 require.Equal(t, healthy, restoredSibling)
354- require.Equal(t, ports.SessionStopped, d.stopped[healthy.Name].state)
354+ require.Equal(t, ports.SessionDown, d.stopped[healthy.Name].state)
355355 require.Equal(t, 1, repository.loads[healthy.Name])
356356 }
357357
@@ -381,7 +381,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
381381 d.mu.Lock()
382382 entry := d.stopped[record.Name]
383383 d.mu.Unlock()
384- require.Equal(t, ports.SessionStopped, entry.state)
384+ require.Equal(t, ports.SessionDown, entry.state)
385385 require.Equal(t, fresh, entry.record)
386386 require.Equal(t, restoreDone, entry.restoreDone)
387387 select {
@@ -394,7 +394,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
394394 d.mu.Lock()
395395 entry = d.stopped[record.Name]
396396 d.mu.Unlock()
397- require.Equal(t, ports.SessionStopped, entry.state)
397+ require.Equal(t, ports.SessionDown, entry.state)
398398 require.Equal(t, fresh, entry.record)
399399 select {
400400 case <-entry.restoreDone:
@@ -472,7 +472,7 @@ func TestCatalogueRestoreAggregateOver4096(t *testing.T) {
472472
473473 require.Len(t, d.stopped, count)
474474 for _, record := range records {
475- require.Equal(t, ports.SessionStopped, d.stopped[record.Name].state)
475+ require.Equal(t, ports.SessionDown, d.stopped[record.Name].state)
476476 require.Equal(t, 1, repository.loads[record.Name])
477477 }
478478 }
@@ -486,7 +486,7 @@ func TestCatalogueRestoreSingleSessionOver4096(t *testing.T) {
486486 // discovery API and therefore performs exactly one direct catalogue lookup.
487487 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
488488
489- require.Equal(t, ports.SessionStopped, d.stopped[record.Name].state)
489+ require.Equal(t, ports.SessionDown, d.stopped[record.Name].state)
490490 require.Equal(t, 1, repository.loads[record.Name])
491491 }
492492
@@ -709,7 +709,7 @@ func TestCreateNamedSessionRegistrationFailureRollsBackDurableState(t *testing.T
709709 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,
710710 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,
711711 tabNames: append([]string(nil), record.TabNames...), record: record,
712- state: ports.SessionStopped, restoreDone: make(chan struct{}),
712+ state: ports.SessionDown, restoreDone: make(chan struct{}),
713713 }
714714 }
715715 beforeStopped, hadStopped := d.stopped[tc.sessName]
@@ -769,7 +769,7 @@ func TestSessionRecoveryCommand(t *testing.T) {
769769
770770 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")
771771 require.NoError(t, err)
772- require.Equal(t, ports.SessionStopped, d.stopped[degraded.Name].state)
772+ require.Equal(t, ports.SessionDown, d.stopped[degraded.Name].state)
773773 require.Equal(t, fresh, d.stopped[degraded.Name].record)
774774 }
775775
@@ -783,8 +783,8 @@ func TestListShowsDegraded(t *testing.T) {
783783
784784 listed := listSessions(t, d)
785785 require.Equal(t, []ports.SessionInfo{
786- {Name: fresh.Name, State: ports.SessionStopped},
787- {Name: restoring.Name, State: ports.SessionStopped},
786+ {Name: fresh.Name, State: ports.SessionDown},
787+ {Name: restoring.Name, State: ports.SessionDown},
788788 {Name: degraded.Name, State: ports.SessionBroken},
789789 }, listed.Sessions)
790790 }
@@ -795,7 +795,7 @@ func TestListPurgingDominatesRestoringState(t *testing.T) {
795795 d.mu.Lock()
796796 entry := d.stopped[record.Name]
797797 entry.purging = true
798- entry.state = ports.SessionStopped
798+ entry.state = ports.SessionDown
799799 d.stopped[record.Name] = entry
800800 d.mu.Unlock()
801801
@@ -935,7 +935,7 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
935935 state ports.SessionState
936936 message string
937937 }{
938- {name: "healthy record without runtime", state: ports.SessionStopped, message: "session was not restored into this daemon: " + record.Name},
938+ {name: "healthy record without runtime", state: ports.SessionDown, message: "session was not restored into this daemon: " + record.Name},
939939 {name: "broken durable state", state: ports.SessionBroken, message: "session durable state is broken: " + record.Name},
940940 }
941941 for _, tt := range tests {
@@ -304,7 +304,7 @@ func TestCatalogueRestoreIndependent(t *testing.T) {
304 healthyEntry := d.stopped[healthy.Name]304 healthyEntry := d.stopped[healthy.Name]
305 brokenEntry := d.stopped[broken.Name]305 brokenEntry := d.stopped[broken.Name]
306 d.mu.Unlock()306 d.mu.Unlock()
307- require.Equal(t, ports.SessionStopped, healthyEntry.state)307+ require.Equal(t, ports.SessionDown, healthyEntry.state)
308 require.Equal(t, ports.SessionBroken, brokenEntry.state)308 require.Equal(t, ports.SessionBroken, brokenEntry.state)
309 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {309 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {
310 select {310 select {
@@ -344,14 +344,14 @@ func TestCatalogueRestoreResetsOnlyIncompatibleCheckpoint(t *testing.T) {
344 require.Empty(t, fresh.DegradedReason)344 require.Empty(t, fresh.DegradedReason)
345 require.Empty(t, fresh.TabNames)345 require.Empty(t, fresh.TabNames)
346 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)346 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)
347- require.Equal(t, ports.SessionStopped, d.stopped[incompatible.Name].state)347+ require.Equal(t, ports.SessionDown, d.stopped[incompatible.Name].state)
348 require.Equal(t, fresh, d.stopped[incompatible.Name].record)348 require.Equal(t, fresh, d.stopped[incompatible.Name].record)
349 349
350 restoredSibling, ok, err := catalogue.Record(healthy.Name)350 restoredSibling, ok, err := catalogue.Record(healthy.Name)
351 require.NoError(t, err)351 require.NoError(t, err)
352 require.True(t, ok)352 require.True(t, ok)
353 require.Equal(t, healthy, restoredSibling)353 require.Equal(t, healthy, restoredSibling)
354- require.Equal(t, ports.SessionStopped, d.stopped[healthy.Name].state)354+ require.Equal(t, ports.SessionDown, d.stopped[healthy.Name].state)
355 require.Equal(t, 1, repository.loads[healthy.Name])355 require.Equal(t, 1, repository.loads[healthy.Name])
356 }356 }
357 357
@@ -381,7 +381,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
381 d.mu.Lock()381 d.mu.Lock()
382 entry := d.stopped[record.Name]382 entry := d.stopped[record.Name]
383 d.mu.Unlock()383 d.mu.Unlock()
384- require.Equal(t, ports.SessionStopped, entry.state)384+ require.Equal(t, ports.SessionDown, entry.state)
385 require.Equal(t, fresh, entry.record)385 require.Equal(t, fresh, entry.record)
386 require.Equal(t, restoreDone, entry.restoreDone)386 require.Equal(t, restoreDone, entry.restoreDone)
387 select {387 select {
@@ -394,7 +394,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
394 d.mu.Lock()394 d.mu.Lock()
395 entry = d.stopped[record.Name]395 entry = d.stopped[record.Name]
396 d.mu.Unlock()396 d.mu.Unlock()
397- require.Equal(t, ports.SessionStopped, entry.state)397+ require.Equal(t, ports.SessionDown, entry.state)
398 require.Equal(t, fresh, entry.record)398 require.Equal(t, fresh, entry.record)
399 select {399 select {
400 case <-entry.restoreDone:400 case <-entry.restoreDone:
@@ -472,7 +472,7 @@ func TestCatalogueRestoreAggregateOver4096(t *testing.T) {
472 472
473 require.Len(t, d.stopped, count)473 require.Len(t, d.stopped, count)
474 for _, record := range records {474 for _, record := range records {
475- require.Equal(t, ports.SessionStopped, d.stopped[record.Name].state)475+ require.Equal(t, ports.SessionDown, d.stopped[record.Name].state)
476 require.Equal(t, 1, repository.loads[record.Name])476 require.Equal(t, 1, repository.loads[record.Name])
477 }477 }
478 }478 }
@@ -486,7 +486,7 @@ func TestCatalogueRestoreSingleSessionOver4096(t *testing.T) {
486 // discovery API and therefore performs exactly one direct catalogue lookup.486 // discovery API and therefore performs exactly one direct catalogue lookup.
487 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))487 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
488 488
489- require.Equal(t, ports.SessionStopped, d.stopped[record.Name].state)489+ require.Equal(t, ports.SessionDown, d.stopped[record.Name].state)
490 require.Equal(t, 1, repository.loads[record.Name])490 require.Equal(t, 1, repository.loads[record.Name])
491 }491 }
492 492
@@ -709,7 +709,7 @@ func TestCreateNamedSessionRegistrationFailureRollsBackDurableState(t *testing.T
709 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,709 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,
710 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,710 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,
711 tabNames: append([]string(nil), record.TabNames...), record: record,711 tabNames: append([]string(nil), record.TabNames...), record: record,
712- state: ports.SessionStopped, restoreDone: make(chan struct{}),712+ state: ports.SessionDown, restoreDone: make(chan struct{}),
713 }713 }
714 }714 }
715 beforeStopped, hadStopped := d.stopped[tc.sessName]715 beforeStopped, hadStopped := d.stopped[tc.sessName]
@@ -769,7 +769,7 @@ func TestSessionRecoveryCommand(t *testing.T) {
769 769
770 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")770 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")
771 require.NoError(t, err)771 require.NoError(t, err)
772- require.Equal(t, ports.SessionStopped, d.stopped[degraded.Name].state)772+ require.Equal(t, ports.SessionDown, d.stopped[degraded.Name].state)
773 require.Equal(t, fresh, d.stopped[degraded.Name].record)773 require.Equal(t, fresh, d.stopped[degraded.Name].record)
774 }774 }
775 775
@@ -783,8 +783,8 @@ func TestListShowsDegraded(t *testing.T) {
783 783
784 listed := listSessions(t, d)784 listed := listSessions(t, d)
785 require.Equal(t, []ports.SessionInfo{785 require.Equal(t, []ports.SessionInfo{
786- {Name: fresh.Name, State: ports.SessionStopped},786+ {Name: fresh.Name, State: ports.SessionDown},
787- {Name: restoring.Name, State: ports.SessionStopped},787+ {Name: restoring.Name, State: ports.SessionDown},
788 {Name: degraded.Name, State: ports.SessionBroken},788 {Name: degraded.Name, State: ports.SessionBroken},
789 }, listed.Sessions)789 }, listed.Sessions)
790 }790 }
@@ -795,7 +795,7 @@ func TestListPurgingDominatesRestoringState(t *testing.T) {
795 d.mu.Lock()795 d.mu.Lock()
796 entry := d.stopped[record.Name]796 entry := d.stopped[record.Name]
797 entry.purging = true797 entry.purging = true
798- entry.state = ports.SessionStopped798+ entry.state = ports.SessionDown
799 d.stopped[record.Name] = entry799 d.stopped[record.Name] = entry
800 d.mu.Unlock()800 d.mu.Unlock()
801 801
@@ -935,7 +935,7 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
935 state ports.SessionState935 state ports.SessionState
936 message string936 message string
937 }{937 }{
938- {name: "healthy record without runtime", state: ports.SessionStopped, message: "session was not restored into this daemon: " + record.Name},938+ {name: "healthy record without runtime", state: ports.SessionDown, message: "session was not restored into this daemon: " + record.Name},
939 {name: "broken durable state", state: ports.SessionBroken, message: "session durable state is broken: " + record.Name},939 {name: "broken durable state", state: ports.SessionBroken, message: "session durable state is broken: " + record.Name},
940 }940 }
941 for _, tt := range tests {941 for _, tt := range tests {
M internal/usecase/daemon/navigation_attachment_handoff_test.go +2 -2
@@ -56,7 +56,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
5656 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffectTicket) error {
5757 d.mu.Lock()
5858 delete(d.sessions, target.id)
59- d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 9, state: ports.SessionStopped}
59+ d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 9, state: ports.SessionDown}
6060 d.mu.Unlock()
6161 expectedCreatedAt := int64(9)
6262 return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")
@@ -149,7 +149,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
149149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {
150150 d, source, old, _ := newManualSessionWithPTYs(t, nil)
151151 d.mu.Lock()
152- d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionStopped}
152+ d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionDown}
153153 d.mu.Unlock()
154154
155155 rc := d.attachCoordinator(source, nil, old, true)
@@ -56,7 +56,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
56 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffectTicket) error {56 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffectTicket) error {
57 d.mu.Lock()57 d.mu.Lock()
58 delete(d.sessions, target.id)58 delete(d.sessions, target.id)
59- d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 9, state: ports.SessionStopped}59+ d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 9, state: ports.SessionDown}
60 d.mu.Unlock()60 d.mu.Unlock()
61 expectedCreatedAt := int64(9)61 expectedCreatedAt := int64(9)
62 return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")62 return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")
@@ -149,7 +149,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {
150 d, source, old, _ := newManualSessionWithPTYs(t, nil)150 d, source, old, _ := newManualSessionWithPTYs(t, nil)
151 d.mu.Lock()151 d.mu.Lock()
152- d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionStopped}152+ d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionDown}
153 d.mu.Unlock()153 d.mu.Unlock()
154 154
155 rc := d.attachCoordinator(source, nil, old, true)155 rc := d.attachCoordinator(source, nil, old, true)
M internal/usecase/daemon/picker.go +4 -4
@@ -252,8 +252,8 @@ func remotePickerReasonText(reason string) string {
252252 return "host unreachable"
253253 case "version_mismatch":
254254 return "version mismatch"
255- case "session_stopped":
256- return "session stopped"
255+ case "session_down":
256+ return "session down"
257257 case "session_broken":
258258 return "session broken"
259259 case "refreshing":
@@ -543,7 +543,7 @@ func (d *Daemon) startRemotePickerPreview(ac *attachedClient, target picker.Targ
543543 remoteTarget := *target.RemoteTarget
544544 message := "loading remote preview…"
545545 if remoteTarget.Stopped {
546- message = "stopped session — preview unavailable"
546+ message = "down session — preview unavailable"
547547 }
548548 ac.overlays.pickerRemotePreview = staticRemotePickerPreview(width, height, message)
549549 if remoteTarget.Stopped {
@@ -980,7 +980,7 @@ func (d *Daemon) remotePickerTargetReadyTarget(target domain.RemoteSessionTarget
980980 if session.Name != target.SessionName || session.LifecycleID != target.LifecycleID {
981981 continue
982982 }
983- if target.Stopped != (session.State == "stopped") || session.State == "broken" {
983+ if target.Stopped != (session.State == "down") || session.State == "broken" {
984984 return false
985985 }
986986 tabs := ports.CatalogTabs(session)
@@ -252,8 +252,8 @@ func remotePickerReasonText(reason string) string {
252 return "host unreachable"252 return "host unreachable"
253 case "version_mismatch":253 case "version_mismatch":
254 return "version mismatch"254 return "version mismatch"
255- case "session_stopped":255+ case "session_down":
256- return "session stopped"256+ return "session down"
257 case "session_broken":257 case "session_broken":
258 return "session broken"258 return "session broken"
259 case "refreshing":259 case "refreshing":
@@ -543,7 +543,7 @@ func (d *Daemon) startRemotePickerPreview(ac *attachedClient, target picker.Targ
543 remoteTarget := *target.RemoteTarget543 remoteTarget := *target.RemoteTarget
544 message := "loading remote preview…"544 message := "loading remote preview…"
545 if remoteTarget.Stopped {545 if remoteTarget.Stopped {
546- message = "stopped session — preview unavailable"546+ message = "down session — preview unavailable"
547 }547 }
548 ac.overlays.pickerRemotePreview = staticRemotePickerPreview(width, height, message)548 ac.overlays.pickerRemotePreview = staticRemotePickerPreview(width, height, message)
549 if remoteTarget.Stopped {549 if remoteTarget.Stopped {
@@ -980,7 +980,7 @@ func (d *Daemon) remotePickerTargetReadyTarget(target domain.RemoteSessionTarget
980 if session.Name != target.SessionName || session.LifecycleID != target.LifecycleID {980 if session.Name != target.SessionName || session.LifecycleID != target.LifecycleID {
981 continue981 continue
982 }982 }
983- if target.Stopped != (session.State == "stopped") || session.State == "broken" {983+ if target.Stopped != (session.State == "down") || session.State == "broken" {
984 return false984 return false
985 }985 }
986 tabs := ports.CatalogTabs(session)986 tabs := ports.CatalogTabs(session)
M internal/usecase/daemon/picker_test.go +2 -2
@@ -741,7 +741,7 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
741741 runtimeState ports.SessionState
742742 }{
743743 {name: "broken", broken: true, runtimeState: ports.SessionBroken},
744- {name: "healthy without restored runtime", runtimeState: ports.SessionStopped},
744+ {name: "healthy without restored runtime", runtimeState: ports.SessionDown},
745745 }
746746
747747 for _, tt := range tests {
@@ -800,7 +800,7 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
800800 defer release3()
801801 d, from, ac, sends := newManualSessionWithPTYs(t, p1)
802802 d.ptys = newFactorySeq(t, p2, p3)
803- d.stopped["work"] = stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionStopped}
803+ d.stopped["work"] = stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionDown}
804804
805805 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})
806806 awaitFrame(t, sends, ports.MsgOutput)
@@ -741,7 +741,7 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
741 runtimeState ports.SessionState741 runtimeState ports.SessionState
742 }{742 }{
743 {name: "broken", broken: true, runtimeState: ports.SessionBroken},743 {name: "broken", broken: true, runtimeState: ports.SessionBroken},
744- {name: "healthy without restored runtime", runtimeState: ports.SessionStopped},744+ {name: "healthy without restored runtime", runtimeState: ports.SessionDown},
745 }745 }
746 746
747 for _, tt := range tests {747 for _, tt := range tests {
@@ -800,7 +800,7 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
800 defer release3()800 defer release3()
801 d, from, ac, sends := newManualSessionWithPTYs(t, p1)801 d, from, ac, sends := newManualSessionWithPTYs(t, p1)
802 d.ptys = newFactorySeq(t, p2, p3)802 d.ptys = newFactorySeq(t, p2, p3)
803- d.stopped["work"] = stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionStopped}803+ d.stopped["work"] = stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionDown}
804 804
805 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})805 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})
806 awaitFrame(t, sends, ports.MsgOutput)806 awaitFrame(t, sends, ports.MsgOutput)
M internal/usecase/daemon/remote_attach_test.go +1 -1
@@ -31,7 +31,7 @@ func TestRemotePickerSelectionRejectsReplacedLifecycle(t *testing.T) {
3131 cachedLifecycle[0]++
3232 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
3333 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
34- LifecycleID: cachedLifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
34+ LifecycleID: cachedLifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
3535 }},
3636 }})
3737 d.remoteCatalog.mu.Lock()
@@ -31,7 +31,7 @@ func TestRemotePickerSelectionRejectsReplacedLifecycle(t *testing.T) {
31 cachedLifecycle[0]++31 cachedLifecycle[0]++
32 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{32 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
33 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{33 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
34- LifecycleID: cachedLifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},34+ LifecycleID: cachedLifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
35 }},35 }},
36 }})36 }})
37 d.remoteCatalog.mu.Lock()37 d.remoteCatalog.mu.Lock()
M internal/usecase/daemon/remote_catalog_state_test.go +4 -4
@@ -42,9 +42,9 @@ func TestRemoteCatalogCacheStartup(t *testing.T) {
4242 {
4343 name: "loads cached entries",
4444 load: func() ([]ports.RemoteCatalogCacheEntry, error) {
45- return []ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}}, nil
45+ return []ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}}, nil
4646 },
47- wantCache: map[string]ports.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}},
47+ wantCache: map[string]ports.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}},
4848 wantStatus: map[string]remoteHostStatus{"arch": remoteHostCached},
4949 },
5050 {
@@ -101,14 +101,14 @@ func TestRemoteCatalogCacheStoreSerializesLockFreeIO(t *testing.T) {
101101 }}
102102 d = New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, cache, nil, ports.RemoteTransportUDP))
103103 d.remoteCatalog.mu.Lock()
104- d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []ports.RemoteCatalogSession{{Name: "one", State: "running"}}}
104+ d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []ports.RemoteCatalogSession{{Name: "one", State: "up"}}}
105105 d.remoteCatalog.mu.Unlock()
106106
107107 done := make(chan struct{}, 2)
108108 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()
109109 <-entered
110110 d.remoteCatalog.mu.Lock()
111- d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []ports.RemoteCatalogSession{{Name: "two", State: "running"}}}
111+ d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []ports.RemoteCatalogSession{{Name: "two", State: "up"}}}
112112 d.remoteCatalog.mu.Unlock()
113113 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()
114114
@@ -42,9 +42,9 @@ func TestRemoteCatalogCacheStartup(t *testing.T) {
42 {42 {
43 name: "loads cached entries",43 name: "loads cached entries",
44 load: func() ([]ports.RemoteCatalogCacheEntry, error) {44 load: func() ([]ports.RemoteCatalogCacheEntry, error) {
45- return []ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}}, nil45+ return []ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}}, nil
46 },46 },
47- wantCache: map[string]ports.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}},47+ wantCache: map[string]ports.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}},
48 wantStatus: map[string]remoteHostStatus{"arch": remoteHostCached},48 wantStatus: map[string]remoteHostStatus{"arch": remoteHostCached},
49 },49 },
50 {50 {
@@ -101,14 +101,14 @@ func TestRemoteCatalogCacheStoreSerializesLockFreeIO(t *testing.T) {
101 }}101 }}
102 d = New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, cache, nil, ports.RemoteTransportUDP))102 d = New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, cache, nil, ports.RemoteTransportUDP))
103 d.remoteCatalog.mu.Lock()103 d.remoteCatalog.mu.Lock()
104- d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []ports.RemoteCatalogSession{{Name: "one", State: "running"}}}104+ d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []ports.RemoteCatalogSession{{Name: "one", State: "up"}}}
105 d.remoteCatalog.mu.Unlock()105 d.remoteCatalog.mu.Unlock()
106 106
107 done := make(chan struct{}, 2)107 done := make(chan struct{}, 2)
108 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()108 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()
109 <-entered109 <-entered
110 d.remoteCatalog.mu.Lock()110 d.remoteCatalog.mu.Lock()
111- d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []ports.RemoteCatalogSession{{Name: "two", State: "running"}}}111+ d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []ports.RemoteCatalogSession{{Name: "two", State: "up"}}}
112 d.remoteCatalog.mu.Unlock()112 d.remoteCatalog.mu.Unlock()
113 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()113 go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }()
114 114
M internal/usecase/daemon/remote_picker.go +5 -5
@@ -133,7 +133,7 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
133133 key.LifecycleID = session.LifecycleID
134134 key.DisplayOrigin = remoteSessionOrigin(key.Host)
135135 availability := remotePickerAvailability(status)
136- stopped := session.State == "stopped"
136+ stopped := session.State == "down"
137137 broken := session.State == "broken"
138138 tabs := ports.CatalogTabs(session)
139139 viewTabs := make([]picker.TabEntry, 0, len(tabs))
@@ -201,13 +201,13 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
201201 detail := remotePickerStatusDetail(status, fetchedAt)
202202 if detail == "" {
203203 switch session.State {
204- case "running":
205- detail = "running"
204+ case "up":
205+ detail = "up"
206206 if session.Attached {
207207 detail += " · attached"
208208 }
209- case "stopped":
210- detail = "stopped"
209+ case "down":
210+ detail = "down"
211211 case "broken":
212212 detail = "broken"
213213 }
@@ -133,7 +133,7 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
133 key.LifecycleID = session.LifecycleID133 key.LifecycleID = session.LifecycleID
134 key.DisplayOrigin = remoteSessionOrigin(key.Host)134 key.DisplayOrigin = remoteSessionOrigin(key.Host)
135 availability := remotePickerAvailability(status)135 availability := remotePickerAvailability(status)
136- stopped := session.State == "stopped"136+ stopped := session.State == "down"
137 broken := session.State == "broken"137 broken := session.State == "broken"
138 tabs := ports.CatalogTabs(session)138 tabs := ports.CatalogTabs(session)
139 viewTabs := make([]picker.TabEntry, 0, len(tabs))139 viewTabs := make([]picker.TabEntry, 0, len(tabs))
@@ -201,13 +201,13 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
201 detail := remotePickerStatusDetail(status, fetchedAt)201 detail := remotePickerStatusDetail(status, fetchedAt)
202 if detail == "" {202 if detail == "" {
203 switch session.State {203 switch session.State {
204- case "running":204+ case "up":
205- detail = "running"205+ detail = "up"
206 if session.Attached {206 if session.Attached {
207 detail += " · attached"207 detail += " · attached"
208 }208 }
209- case "stopped":209+ case "down":
210- detail = "stopped"210+ detail = "down"
211 case "broken":211 case "broken":
212 detail = "broken"212 detail = "broken"
213 }213 }
M internal/usecase/daemon/remote_picker_test.go +52 -28
@@ -64,7 +64,7 @@ func TestRemotePickerCatalogExpiresAtAttachTTL(t *testing.T) {
6464 lifecycle := remoteLifecycleForTest()
6565 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
6666 Host: "arch", FetchedAt: now.Add(-remotePickerAttachTTL), Sessions: []ports.RemoteCatalogSession{{
67- LifecycleID: lifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
67+ LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
6868 }},
6969 }})
7070 d.remoteCatalog.mu.Lock()
@@ -85,7 +85,7 @@ func TestRemotePickerTargetReadyExpiresAfterPickerBuildWithoutSnapshot(t *testin
8585 lifecycle := remoteLifecycleForTest()
8686 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
8787 Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{
88- LifecycleID: lifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
88+ LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
8989 }},
9090 }})
9191 d.remoteCatalog.mu.Lock()
@@ -152,7 +152,7 @@ func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) {
152152 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
153153 LifecycleID: lifecycle,
154154 Name: "vive",
155- State: "running",
155+ State: "up",
156156 Tabs: test.tabs,
157157 ActiveTabID: test.activeTabID,
158158 }},
@@ -191,7 +191,7 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
191191 t.Run(test.name, func(t *testing.T) {
192192 d := newRemotePickerDaemon(nil)
193193 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
194- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}},
194+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}},
195195 }})
196196 d.remoteCatalog.mu.Lock()
197197 d.remoteCatalog.status["arch"] = test.status
@@ -205,12 +205,36 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
205205 }
206206 }
207207
208+func TestRemotePickerViewUsesShortStateLabels(t *testing.T) {
209+ key := domain.RemoteSessionKey{Host: "arch", Name: "work"}
210+ lifecycle := remoteLifecycleForTest()
211+ for _, test := range []struct {
212+ state string
213+ detail string
214+ stopped bool
215+ }{
216+ {state: "up", detail: "up"},
217+ {state: "down", detail: "down", stopped: true},
218+ } {
219+ t.Run(test.state, func(t *testing.T) {
220+ view := remotePickerView(key, ports.RemoteCatalogSession{
221+ LifecycleID: lifecycle,
222+ Name: key.Name,
223+ State: test.state,
224+ Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
225+ }, remoteHostFresh, time.Unix(1, 0))
226+ require.Equal(t, test.detail, view.RemoteDetail)
227+ require.Equal(t, test.stopped, view.Stopped)
228+ })
229+ }
230+}
231+
208232 func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) {
209233 d := newRemotePickerDaemon(nil)
210234 fetchedAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
211235 failedAt := fetchedAt.Add(6 * time.Hour)
212236 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
213- Host: "arch", FetchedAt: fetchedAt, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}},
237+ Host: "arch", FetchedAt: fetchedAt, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}},
214238 }})
215239 d.remoteCatalog.mu.Lock()
216240 d.remoteCatalog.status["arch"] = remoteHostUnreachable
@@ -393,7 +417,7 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T)
393417 ac.overlays.pickerMu.Unlock()
394418 require.True(t, d.registerRemotePicker(instance))
395419 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
396- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "cached", State: "running"}},
420+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "cached", State: "up"}},
397421 }})
398422
399423 firstGeneration := d.startRemotePickerRefresh(instance)
@@ -406,7 +430,7 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T)
406430 require.Contains(t, first, "mule")
407431 requireNoRemoteLockViolations(t, catalog, nil)
408432 d.remoteCatalog.mu.Lock()
409- require.Equal(t, []ports.RemoteCatalogSession{{Name: "cached", State: "running"}}, d.remoteCatalog.cache["arch"].Sessions, "cached rows publish before any remote completion")
433+ require.Equal(t, []ports.RemoteCatalogSession{{Name: "cached", State: "up"}}, d.remoteCatalog.cache["arch"].Sessions, "cached rows publish before any remote completion")
410434 require.Equal(t, remoteHostRefreshing, d.remoteCatalog.status["arch"])
411435 d.remoteCatalog.mu.Unlock()
412436
@@ -442,7 +466,7 @@ func TestRemotePickerRefreshDisablesCachedTargetUntilCatalogCompletes(t *testing
442466 Sessions: []ports.RemoteCatalogSession{{
443467 LifecycleID: lifecycle,
444468 Name: "work",
445- State: "running",
469+ State: "up",
446470 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
447471 }},
448472 }})
@@ -472,7 +496,7 @@ func TestRemotePickerRefreshDisablesCachedTargetUntilCatalogCompletes(t *testing
472496 request.result <- remoteRefreshResult{catalog: ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
473497 LifecycleID: lifecycle,
474498 Name: "work",
475- State: "running",
499+ State: "up",
476500 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
477501 }}}}
478502 receiveRemotePicker(t, cache.stores, "remote cache store")
@@ -525,8 +549,8 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
525549 hosts := &remoteRefreshHostStore{hosts: []string{"arch", "mule"}}
526550 d, _, cache := newRemoteRefreshDaemon(t, hosts, now)
527551 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{
528- {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "running"}}},
529- {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}},
552+ {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}}},
553+ {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}},
530554 })
531555 d.remoteCatalog.mu.Lock()
532556 d.remoteCatalog.refresh = 7
@@ -534,7 +558,7 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
534558
535559 require.False(t, d.applyRemoteRefreshResult(7, "mule", ports.RemoteCatalog{}, errors.New("offline")))
536560 d.remoteCatalog.mu.Lock()
537- require.Equal(t, []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}, d.remoteCatalog.cache["mule"].Sessions)
561+ require.Equal(t, []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}, d.remoteCatalog.cache["mule"].Sessions)
538562 require.Equal(t, time.Unix(20, 0), d.remoteCatalog.cache["mule"].FetchedAt)
539563 require.Equal(t, remoteHostUnreachable, d.remoteCatalog.status["mule"])
540564 d.remoteCatalog.mu.Unlock()
@@ -543,7 +567,7 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
543567 stored := receiveRemotePicker(t, cache.stores, "remote cache store")
544568 require.Equal(t, []ports.RemoteCatalogCacheEntry{
545569 {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{}},
546- {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}},
570+ {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}},
547571 }, stored, "every successful write persists the newest full cache atomically")
548572 requireNoRemoteLockViolations(t, nil, cache)
549573 d.remoteCatalog.mu.Lock()
@@ -556,15 +580,15 @@ func TestRemoteRefreshRejectsRemovedHostAndLateGeneration(t *testing.T) {
556580 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
557581 d, _, cache := newRemoteRefreshDaemon(t, hosts, time.Unix(300, 0))
558582 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
559- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "running"}},
583+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}},
560584 }})
561585 d.remoteCatalog.mu.Lock()
562586 d.remoteCatalog.refresh = 9
563587 d.remoteCatalog.mu.Unlock()
564588
565- require.False(t, d.applyRemoteRefreshResult(8, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "late", State: "running"}}}, nil))
589+ require.False(t, d.applyRemoteRefreshResult(8, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "late", State: "up"}}}, nil))
566590 hosts.set()
567- require.True(t, d.applyRemoteRefreshResult(9, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "removed", State: "running"}}}, nil))
591+ require.True(t, d.applyRemoteRefreshResult(9, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "removed", State: "up"}}}, nil))
568592 require.Empty(t, receiveRemotePicker(t, cache.stores, "remote cache store"))
569593 requireNoRemoteLockViolations(t, nil, cache)
570594 d.remoteCatalog.mu.Lock()
@@ -580,7 +604,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
580604 session ports.RemoteCatalogSession
581605 }{
582606 {name: "unknown state", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "unknown"}},
583- {name: "invalid reason", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "running", Reason: "bogus"}},
607+ {name: "invalid reason", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "up", Reason: "bogus"}},
584608 } {
585609 t.Run(test.name, func(t *testing.T) {
586610 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
@@ -598,7 +622,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
598622 func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) {
599623 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
600624 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))
601- old := ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}
625+ old := ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}
602626 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{old})
603627 d.remoteCatalog.mu.Lock()
604628 d.remoteCatalog.refresh = 3
@@ -617,7 +641,7 @@ func TestRemotePickerSelectionKeepsLocalConnectionUntilRemoteViewIsReady(t *test
617641 require.NoError(t, target.Validate())
618642 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
619643 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
620- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
644+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
621645 }},
622646 }})
623647 d.remoteCatalog.mu.Lock()
@@ -710,8 +734,8 @@ func TestRemotePickerSelectionTransitionsBetweenRemoteViewsAndSharesAttachmentMR
710734 second.LiveTabID = "tab-2"
711735 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
712736 Host: first.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{
713- {LifecycleID: first.LifecycleID, Name: first.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(first.LiveTabID), Index: 0, Name: "main"}}},
714- {LifecycleID: second.LifecycleID, Name: second.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(second.LiveTabID), Index: 0, Name: "main"}}},
737+ {LifecycleID: first.LifecycleID, Name: first.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(first.LiveTabID), Index: 0, Name: "main"}}},
738+ {LifecycleID: second.LifecycleID, Name: second.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(second.LiveTabID), Index: 0, Name: "main"}}},
715739 },
716740 }})
717741 d.remoteCatalog.mu.Lock()
@@ -800,7 +824,7 @@ func TestRemotePickerReselectUsesWarmRemoteViewWithoutSecondDial(t *testing.T) {
800824 target := remoteLinkTestTarget()
801825 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
802826 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
803- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
827+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
804828 }},
805829 }})
806830 d.remoteCatalog.mu.Lock()
@@ -879,7 +903,7 @@ func TestRemotePickerWarmReselectActivatesSelectedAlternateTabWithoutSecondDial(
879903 const alternateName = "alternate"
880904 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
881905 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
882- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running",
906+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up",
883907 Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}, {ID: string(alternate.LiveTabID), Index: 1, Name: alternateName}},
884908 }},
885909 }})
@@ -963,7 +987,7 @@ func TestRemotePickerSelectionFailureKeepsSourceAndPickerOpen(t *testing.T) {
963987 target := remoteLinkTestTarget()
964988 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
965989 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
966- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
990+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
967991 }},
968992 }})
969993 d.remoteCatalog.mu.Lock()
@@ -1012,7 +1036,7 @@ func TestRemotePickerSelectionEscapeCancelsWithoutDetachingSource(t *testing.T)
10121036 target := remoteLinkTestTarget()
10131037 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
10141038 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
1015- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
1039+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
10161040 }},
10171041 }})
10181042 d.remoteCatalog.mu.Lock()
@@ -1097,7 +1121,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
10971121
10981122 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{
10991123 ProtocolVersion: ports.ProtocolVersion,
1100- Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running", Tabs: 2}},
1124+ Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up", Tabs: 2}},
11011125 }, nil))
11021126 receiveRemotePicker(t, cache.stores, "remote cache store")
11031127 requireNoRemoteLockViolations(t, catalog, cache)
@@ -1410,7 +1434,7 @@ func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
14101434 func TestRemoteRowsRequireFreshRichIdentityForPickerActivation(t *testing.T) {
14111435 key := domain.RemoteSessionKey{Host: "arch", Name: "work"}
14121436 session := ports.RemoteCatalogSession{
1413- LifecycleID: remoteLifecycleForTest(), Name: key.Name, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Name: "main"}},
1437+ LifecycleID: remoteLifecycleForTest(), Name: key.Name, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Name: "main"}},
14141438 }
14151439 for _, test := range []struct {
14161440 status remoteHostStatus
@@ -1430,7 +1454,7 @@ func TestRemoteRowsRequireFreshRichIdentityForPickerActivation(t *testing.T) {
14301454 require.Equal(t, test.want, view.RemoteAttachReady, "status %d", test.status)
14311455 }
14321456
1433- legacy := remotePickerView(key, ports.RemoteCatalogSession{Name: key.Name, State: "running"}, remoteHostFresh, time.Unix(1, 0))
1457+ legacy := remotePickerView(key, ports.RemoteCatalogSession{Name: key.Name, State: "up"}, remoteHostFresh, time.Unix(1, 0))
14341458 require.False(t, legacy.RemoteAttachReady, "count-only cached rows cannot initiate proxied picker activation")
14351459 _, selectable := picker.New([]picker.SessionView{legacy}, picker.SelectionConfig{Mode: picker.SelectNavigationTab}).Selected()
14361460 require.False(t, selectable)
@@ -64,7 +64,7 @@ func TestRemotePickerCatalogExpiresAtAttachTTL(t *testing.T) {
64 lifecycle := remoteLifecycleForTest()64 lifecycle := remoteLifecycleForTest()
65 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{65 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
66 Host: "arch", FetchedAt: now.Add(-remotePickerAttachTTL), Sessions: []ports.RemoteCatalogSession{{66 Host: "arch", FetchedAt: now.Add(-remotePickerAttachTTL), Sessions: []ports.RemoteCatalogSession{{
67- LifecycleID: lifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},67+ LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
68 }},68 }},
69 }})69 }})
70 d.remoteCatalog.mu.Lock()70 d.remoteCatalog.mu.Lock()
@@ -85,7 +85,7 @@ func TestRemotePickerTargetReadyExpiresAfterPickerBuildWithoutSnapshot(t *testin
85 lifecycle := remoteLifecycleForTest()85 lifecycle := remoteLifecycleForTest()
86 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{86 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
87 Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{87 Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{
88- LifecycleID: lifecycle, Name: "work", State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},88+ LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
89 }},89 }},
90 }})90 }})
91 d.remoteCatalog.mu.Lock()91 d.remoteCatalog.mu.Lock()
@@ -152,7 +152,7 @@ func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) {
152 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{152 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
153 LifecycleID: lifecycle,153 LifecycleID: lifecycle,
154 Name: "vive",154 Name: "vive",
155- State: "running",155+ State: "up",
156 Tabs: test.tabs,156 Tabs: test.tabs,
157 ActiveTabID: test.activeTabID,157 ActiveTabID: test.activeTabID,
158 }},158 }},
@@ -191,7 +191,7 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
191 t.Run(test.name, func(t *testing.T) {191 t.Run(test.name, func(t *testing.T) {
192 d := newRemotePickerDaemon(nil)192 d := newRemotePickerDaemon(nil)
193 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{193 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
194- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}},194+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}},
195 }})195 }})
196 d.remoteCatalog.mu.Lock()196 d.remoteCatalog.mu.Lock()
197 d.remoteCatalog.status["arch"] = test.status197 d.remoteCatalog.status["arch"] = test.status
@@ -205,12 +205,36 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
205 }205 }
206 }206 }
207 207
208+func TestRemotePickerViewUsesShortStateLabels(t *testing.T) {
209+ key := domain.RemoteSessionKey{Host: "arch", Name: "work"}
210+ lifecycle := remoteLifecycleForTest()
211+ for _, test := range []struct {
212+ state string
213+ detail string
214+ stopped bool
215+ }{
216+ {state: "up", detail: "up"},
217+ {state: "down", detail: "down", stopped: true},
218+ } {
219+ t.Run(test.state, func(t *testing.T) {
220+ view := remotePickerView(key, ports.RemoteCatalogSession{
221+ LifecycleID: lifecycle,
222+ Name: key.Name,
223+ State: test.state,
224+ Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
225+ }, remoteHostFresh, time.Unix(1, 0))
226+ require.Equal(t, test.detail, view.RemoteDetail)
227+ require.Equal(t, test.stopped, view.Stopped)
228+ })
229+ }
230+}
231+
208 func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) {232 func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) {
209 d := newRemotePickerDaemon(nil)233 d := newRemotePickerDaemon(nil)
210 fetchedAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)234 fetchedAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
211 failedAt := fetchedAt.Add(6 * time.Hour)235 failedAt := fetchedAt.Add(6 * time.Hour)
212 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{236 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
213- Host: "arch", FetchedAt: fetchedAt, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}},237+ Host: "arch", FetchedAt: fetchedAt, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}},
214 }})238 }})
215 d.remoteCatalog.mu.Lock()239 d.remoteCatalog.mu.Lock()
216 d.remoteCatalog.status["arch"] = remoteHostUnreachable240 d.remoteCatalog.status["arch"] = remoteHostUnreachable
@@ -393,7 +417,7 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T)
393 ac.overlays.pickerMu.Unlock()417 ac.overlays.pickerMu.Unlock()
394 require.True(t, d.registerRemotePicker(instance))418 require.True(t, d.registerRemotePicker(instance))
395 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{419 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
396- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "cached", State: "running"}},420+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "cached", State: "up"}},
397 }})421 }})
398 422
399 firstGeneration := d.startRemotePickerRefresh(instance)423 firstGeneration := d.startRemotePickerRefresh(instance)
@@ -406,7 +430,7 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T)
406 require.Contains(t, first, "mule")430 require.Contains(t, first, "mule")
407 requireNoRemoteLockViolations(t, catalog, nil)431 requireNoRemoteLockViolations(t, catalog, nil)
408 d.remoteCatalog.mu.Lock()432 d.remoteCatalog.mu.Lock()
409- require.Equal(t, []ports.RemoteCatalogSession{{Name: "cached", State: "running"}}, d.remoteCatalog.cache["arch"].Sessions, "cached rows publish before any remote completion")433+ require.Equal(t, []ports.RemoteCatalogSession{{Name: "cached", State: "up"}}, d.remoteCatalog.cache["arch"].Sessions, "cached rows publish before any remote completion")
410 require.Equal(t, remoteHostRefreshing, d.remoteCatalog.status["arch"])434 require.Equal(t, remoteHostRefreshing, d.remoteCatalog.status["arch"])
411 d.remoteCatalog.mu.Unlock()435 d.remoteCatalog.mu.Unlock()
412 436
@@ -442,7 +466,7 @@ func TestRemotePickerRefreshDisablesCachedTargetUntilCatalogCompletes(t *testing
442 Sessions: []ports.RemoteCatalogSession{{466 Sessions: []ports.RemoteCatalogSession{{
443 LifecycleID: lifecycle,467 LifecycleID: lifecycle,
444 Name: "work",468 Name: "work",
445- State: "running",469+ State: "up",
446 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},470 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
447 }},471 }},
448 }})472 }})
@@ -472,7 +496,7 @@ func TestRemotePickerRefreshDisablesCachedTargetUntilCatalogCompletes(t *testing
472 request.result <- remoteRefreshResult{catalog: ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{496 request.result <- remoteRefreshResult{catalog: ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
473 LifecycleID: lifecycle,497 LifecycleID: lifecycle,
474 Name: "work",498 Name: "work",
475- State: "running",499+ State: "up",
476 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},500 Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
477 }}}}501 }}}}
478 receiveRemotePicker(t, cache.stores, "remote cache store")502 receiveRemotePicker(t, cache.stores, "remote cache store")
@@ -525,8 +549,8 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
525 hosts := &remoteRefreshHostStore{hosts: []string{"arch", "mule"}}549 hosts := &remoteRefreshHostStore{hosts: []string{"arch", "mule"}}
526 d, _, cache := newRemoteRefreshDaemon(t, hosts, now)550 d, _, cache := newRemoteRefreshDaemon(t, hosts, now)
527 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{551 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{
528- {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "running"}}},552+ {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}}},
529- {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}},553+ {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}},
530 })554 })
531 d.remoteCatalog.mu.Lock()555 d.remoteCatalog.mu.Lock()
532 d.remoteCatalog.refresh = 7556 d.remoteCatalog.refresh = 7
@@ -534,7 +558,7 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
534 558
535 require.False(t, d.applyRemoteRefreshResult(7, "mule", ports.RemoteCatalog{}, errors.New("offline")))559 require.False(t, d.applyRemoteRefreshResult(7, "mule", ports.RemoteCatalog{}, errors.New("offline")))
536 d.remoteCatalog.mu.Lock()560 d.remoteCatalog.mu.Lock()
537- require.Equal(t, []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}, d.remoteCatalog.cache["mule"].Sessions)561+ require.Equal(t, []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}, d.remoteCatalog.cache["mule"].Sessions)
538 require.Equal(t, time.Unix(20, 0), d.remoteCatalog.cache["mule"].FetchedAt)562 require.Equal(t, time.Unix(20, 0), d.remoteCatalog.cache["mule"].FetchedAt)
539 require.Equal(t, remoteHostUnreachable, d.remoteCatalog.status["mule"])563 require.Equal(t, remoteHostUnreachable, d.remoteCatalog.status["mule"])
540 d.remoteCatalog.mu.Unlock()564 d.remoteCatalog.mu.Unlock()
@@ -543,7 +567,7 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes
543 stored := receiveRemotePicker(t, cache.stores, "remote cache store")567 stored := receiveRemotePicker(t, cache.stores, "remote cache store")
544 require.Equal(t, []ports.RemoteCatalogCacheEntry{568 require.Equal(t, []ports.RemoteCatalogCacheEntry{
545 {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{}},569 {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{}},
546- {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "running"}}},570+ {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}},
547 }, stored, "every successful write persists the newest full cache atomically")571 }, stored, "every successful write persists the newest full cache atomically")
548 requireNoRemoteLockViolations(t, nil, cache)572 requireNoRemoteLockViolations(t, nil, cache)
549 d.remoteCatalog.mu.Lock()573 d.remoteCatalog.mu.Lock()
@@ -556,15 +580,15 @@ func TestRemoteRefreshRejectsRemovedHostAndLateGeneration(t *testing.T) {
556 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}580 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
557 d, _, cache := newRemoteRefreshDaemon(t, hosts, time.Unix(300, 0))581 d, _, cache := newRemoteRefreshDaemon(t, hosts, time.Unix(300, 0))
558 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{582 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
559- Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "running"}},583+ Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}},
560 }})584 }})
561 d.remoteCatalog.mu.Lock()585 d.remoteCatalog.mu.Lock()
562 d.remoteCatalog.refresh = 9586 d.remoteCatalog.refresh = 9
563 d.remoteCatalog.mu.Unlock()587 d.remoteCatalog.mu.Unlock()
564 588
565- require.False(t, d.applyRemoteRefreshResult(8, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "late", State: "running"}}}, nil))589+ require.False(t, d.applyRemoteRefreshResult(8, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "late", State: "up"}}}, nil))
566 hosts.set()590 hosts.set()
567- require.True(t, d.applyRemoteRefreshResult(9, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "removed", State: "running"}}}, nil))591+ require.True(t, d.applyRemoteRefreshResult(9, "arch", ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, Sessions: []ports.RemoteCatalogSession{{Name: "removed", State: "up"}}}, nil))
568 require.Empty(t, receiveRemotePicker(t, cache.stores, "remote cache store"))592 require.Empty(t, receiveRemotePicker(t, cache.stores, "remote cache store"))
569 requireNoRemoteLockViolations(t, nil, cache)593 requireNoRemoteLockViolations(t, nil, cache)
570 d.remoteCatalog.mu.Lock()594 d.remoteCatalog.mu.Lock()
@@ -580,7 +604,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
580 session ports.RemoteCatalogSession604 session ports.RemoteCatalogSession
581 }{605 }{
582 {name: "unknown state", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "unknown"}},606 {name: "unknown state", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "unknown"}},
583- {name: "invalid reason", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "running", Reason: "bogus"}},607+ {name: "invalid reason", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "up", Reason: "bogus"}},
584 } {608 } {
585 t.Run(test.name, func(t *testing.T) {609 t.Run(test.name, func(t *testing.T) {
586 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}610 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
@@ -598,7 +622,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
598 func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) {622 func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) {
599 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}623 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
600 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))624 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))
601- old := ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running"}}}625+ old := ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}
602 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{old})626 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{old})
603 d.remoteCatalog.mu.Lock()627 d.remoteCatalog.mu.Lock()
604 d.remoteCatalog.refresh = 3628 d.remoteCatalog.refresh = 3
@@ -617,7 +641,7 @@ func TestRemotePickerSelectionKeepsLocalConnectionUntilRemoteViewIsReady(t *test
617 require.NoError(t, target.Validate())641 require.NoError(t, target.Validate())
618 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{642 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
619 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{643 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
620- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},644+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
621 }},645 }},
622 }})646 }})
623 d.remoteCatalog.mu.Lock()647 d.remoteCatalog.mu.Lock()
@@ -710,8 +734,8 @@ func TestRemotePickerSelectionTransitionsBetweenRemoteViewsAndSharesAttachmentMR
710 second.LiveTabID = "tab-2"734 second.LiveTabID = "tab-2"
711 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{735 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
712 Host: first.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{736 Host: first.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{
713- {LifecycleID: first.LifecycleID, Name: first.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(first.LiveTabID), Index: 0, Name: "main"}}},737+ {LifecycleID: first.LifecycleID, Name: first.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(first.LiveTabID), Index: 0, Name: "main"}}},
714- {LifecycleID: second.LifecycleID, Name: second.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(second.LiveTabID), Index: 0, Name: "main"}}},738+ {LifecycleID: second.LifecycleID, Name: second.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(second.LiveTabID), Index: 0, Name: "main"}}},
715 },739 },
716 }})740 }})
717 d.remoteCatalog.mu.Lock()741 d.remoteCatalog.mu.Lock()
@@ -800,7 +824,7 @@ func TestRemotePickerReselectUsesWarmRemoteViewWithoutSecondDial(t *testing.T) {
800 target := remoteLinkTestTarget()824 target := remoteLinkTestTarget()
801 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{825 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
802 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{826 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
803- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},827+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
804 }},828 }},
805 }})829 }})
806 d.remoteCatalog.mu.Lock()830 d.remoteCatalog.mu.Lock()
@@ -879,7 +903,7 @@ func TestRemotePickerWarmReselectActivatesSelectedAlternateTabWithoutSecondDial(
879 const alternateName = "alternate"903 const alternateName = "alternate"
880 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{904 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
881 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{905 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
882- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running",906+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up",
883 Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}, {ID: string(alternate.LiveTabID), Index: 1, Name: alternateName}},907 Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}, {ID: string(alternate.LiveTabID), Index: 1, Name: alternateName}},
884 }},908 }},
885 }})909 }})
@@ -963,7 +987,7 @@ func TestRemotePickerSelectionFailureKeepsSourceAndPickerOpen(t *testing.T) {
963 target := remoteLinkTestTarget()987 target := remoteLinkTestTarget()
964 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{988 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
965 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{989 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
966- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},990+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
967 }},991 }},
968 }})992 }})
969 d.remoteCatalog.mu.Lock()993 d.remoteCatalog.mu.Lock()
@@ -1012,7 +1036,7 @@ func TestRemotePickerSelectionEscapeCancelsWithoutDetachingSource(t *testing.T)
1012 target := remoteLinkTestTarget()1036 target := remoteLinkTestTarget()
1013 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{1037 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
1014 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{1038 Host: target.Endpoint, FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
1015- LifecycleID: target.LifecycleID, Name: target.SessionName, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},1039+ LifecycleID: target.LifecycleID, Name: target.SessionName, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: string(target.LiveTabID), Index: 0, Name: "main"}},
1016 }},1040 }},
1017 }})1041 }})
1018 d.remoteCatalog.mu.Lock()1042 d.remoteCatalog.mu.Lock()
@@ -1097,7 +1121,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
1097 1121
1098 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{1122 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{
1099 ProtocolVersion: ports.ProtocolVersion,1123 ProtocolVersion: ports.ProtocolVersion,
1100- Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "running", Tabs: 2}},1124+ Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up", Tabs: 2}},
1101 }, nil))1125 }, nil))
1102 receiveRemotePicker(t, cache.stores, "remote cache store")1126 receiveRemotePicker(t, cache.stores, "remote cache store")
1103 requireNoRemoteLockViolations(t, catalog, cache)1127 requireNoRemoteLockViolations(t, catalog, cache)
@@ -1410,7 +1434,7 @@ func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
1410 func TestRemoteRowsRequireFreshRichIdentityForPickerActivation(t *testing.T) {1434 func TestRemoteRowsRequireFreshRichIdentityForPickerActivation(t *testing.T) {
1411 key := domain.RemoteSessionKey{Host: "arch", Name: "work"}1435 key := domain.RemoteSessionKey{Host: "arch", Name: "work"}
1412 session := ports.RemoteCatalogSession{1436 session := ports.RemoteCatalogSession{
1413- LifecycleID: remoteLifecycleForTest(), Name: key.Name, State: "running", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Name: "main"}},1437+ LifecycleID: remoteLifecycleForTest(), Name: key.Name, State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Name: "main"}},
1414 }1438 }
1415 for _, test := range []struct {1439 for _, test := range []struct {
1416 status remoteHostStatus1440 status remoteHostStatus
@@ -1430,7 +1454,7 @@ func TestRemoteRowsRequireFreshRichIdentityForPickerActivation(t *testing.T) {
1430 require.Equal(t, test.want, view.RemoteAttachReady, "status %d", test.status)1454 require.Equal(t, test.want, view.RemoteAttachReady, "status %d", test.status)
1431 }1455 }
1432 1456
1433- legacy := remotePickerView(key, ports.RemoteCatalogSession{Name: key.Name, State: "running"}, remoteHostFresh, time.Unix(1, 0))1457+ legacy := remotePickerView(key, ports.RemoteCatalogSession{Name: key.Name, State: "up"}, remoteHostFresh, time.Unix(1, 0))
1434 require.False(t, legacy.RemoteAttachReady, "count-only cached rows cannot initiate proxied picker activation")1458 require.False(t, legacy.RemoteAttachReady, "count-only cached rows cannot initiate proxied picker activation")
1435 _, selectable := picker.New([]picker.SessionView{legacy}, picker.SelectionConfig{Mode: picker.SelectNavigationTab}).Selected()1459 _, selectable := picker.New([]picker.SessionView{legacy}, picker.SelectionConfig{Mode: picker.SelectNavigationTab}).Selected()
1436 require.False(t, selectable)1460 require.False(t, selectable)
M internal/usecase/daemon/session.go +1 -1
@@ -1576,7 +1576,7 @@ func (d *Daemon) killSessionWithSnapshotDeadline(sess *session, reason uint8, pu
15761576 unlockSessions()
15771577 d.notices.routingMu.Unlock()
15781578 if !ephemeral {
1579- stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, incarnation: incarnation, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, tabRecords: tabRecords, record: stoppedRecord, state: ports.SessionStopped, purging: purge}
1579+ stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, incarnation: incarnation, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, tabRecords: tabRecords, record: stoppedRecord, state: ports.SessionDown, purging: purge}
15801580 d.stopped[stoppedName] = stopped
15811581 }
15821582 empty := len(d.sessions) == 0
@@ -1576,7 +1576,7 @@ func (d *Daemon) killSessionWithSnapshotDeadline(sess *session, reason uint8, pu
1576 unlockSessions()1576 unlockSessions()
1577 d.notices.routingMu.Unlock()1577 d.notices.routingMu.Unlock()
1578 if !ephemeral {1578 if !ephemeral {
1579- stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, incarnation: incarnation, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, tabRecords: tabRecords, record: stoppedRecord, state: ports.SessionStopped, purging: purge}1579+ stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, incarnation: incarnation, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, tabRecords: tabRecords, record: stoppedRecord, state: ports.SessionDown, purging: purge}
1580 d.stopped[stoppedName] = stopped1580 d.stopped[stoppedName] = stopped
1581 }1581 }
1582 empty := len(d.sessions) == 01582 empty := len(d.sessions) == 0
M internal/usecase/daemon/session_test.go +2 -2
@@ -779,7 +779,7 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) {
779779 require.Equal(t, int64(7), stopped.createdAt)
780780 require.Equal(t, incarnation, stopped.incarnation)
781781 require.Equal(t, uint64(9), stopped.lastUsedSeq)
782- require.Equal(t, ports.SessionStopped, stopped.state)
782+ require.Equal(t, ports.SessionDown, stopped.state)
783783 require.NotNil(t, stopped.restoreDone)
784784 require.Equal(t, uint64(9), d.mruSeq.Load())
785785 }
@@ -1718,7 +1718,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
17181718
17191719 alpha := d.stopped["alpha"]
17201720 require.Equal(t, records[0], alpha.record)
1721- require.Equal(t, ports.SessionStopped, alpha.state)
1721+ require.Equal(t, ports.SessionDown, alpha.state)
17221722 require.NotNil(t, alpha.restoreDone)
17231723 select {
17241724 case <-alpha.restoreDone:
@@ -779,7 +779,7 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) {
779 require.Equal(t, int64(7), stopped.createdAt)779 require.Equal(t, int64(7), stopped.createdAt)
780 require.Equal(t, incarnation, stopped.incarnation)780 require.Equal(t, incarnation, stopped.incarnation)
781 require.Equal(t, uint64(9), stopped.lastUsedSeq)781 require.Equal(t, uint64(9), stopped.lastUsedSeq)
782- require.Equal(t, ports.SessionStopped, stopped.state)782+ require.Equal(t, ports.SessionDown, stopped.state)
783 require.NotNil(t, stopped.restoreDone)783 require.NotNil(t, stopped.restoreDone)
784 require.Equal(t, uint64(9), d.mruSeq.Load())784 require.Equal(t, uint64(9), d.mruSeq.Load())
785 }785 }
@@ -1718,7 +1718,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
1718 1718
1719 alpha := d.stopped["alpha"]1719 alpha := d.stopped["alpha"]
1720 require.Equal(t, records[0], alpha.record)1720 require.Equal(t, records[0], alpha.record)
1721- require.Equal(t, ports.SessionStopped, alpha.state)1721+ require.Equal(t, ports.SessionDown, alpha.state)
1722 require.NotNil(t, alpha.restoreDone)1722 require.NotNil(t, alpha.restoreDone)
1723 select {1723 select {
1724 case <-alpha.restoreDone:1724 case <-alpha.restoreDone:
M internal/usecase/daemon/snapshot_restore_incremental.go +4 -4
@@ -21,7 +21,7 @@ var errRetryableRestoreLoad = errors.New("snapshot: retryable checkpoint load")
2121
2222 func initialSessionState(record domain.CatalogueRecord) (ports.SessionState, chan struct{}) {
2323 done := make(chan struct{})
24- state := ports.SessionStopped
24+ state := ports.SessionDown
2525 if record.DegradedReason != "" {
2626 state = ports.SessionBroken
2727 close(done)
@@ -179,7 +179,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
179179 return nil
180180 }
181181 if record.Committed == nil {
182- d.setStoppedRecovery(record, ports.SessionStopped)
182+ d.setStoppedRecovery(record, ports.SessionDown)
183183 d.logSessionRestoreComplete(record, 0, false)
184184 return nil
185185 }
@@ -231,7 +231,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
231231 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {
232232 return err
233233 }
234- d.setStoppedRecovery(record, ports.SessionStopped)
234+ d.setStoppedRecovery(record, ports.SessionDown)
235235 d.logSessionRestoreComplete(record, selected.Generation, false)
236236 return nil
237237 }
@@ -252,7 +252,7 @@ func (d *Daemon) resetIncompatibleCheckpoint(ctx context.Context, record domain.
252252
253253 d.mu.Lock()
254254 if entry, ok := d.stopped[record.Name]; ok {
255- d.stopped[record.Name] = stoppedSessionFromRecord(fresh, ports.SessionStopped, entry.restoreDone)
255+ d.stopped[record.Name] = stoppedSessionFromRecord(fresh, ports.SessionDown, entry.restoreDone)
256256 }
257257 d.mu.Unlock()
258258
@@ -21,7 +21,7 @@ var errRetryableRestoreLoad = errors.New("snapshot: retryable checkpoint load")
21 21
22 func initialSessionState(record domain.CatalogueRecord) (ports.SessionState, chan struct{}) {22 func initialSessionState(record domain.CatalogueRecord) (ports.SessionState, chan struct{}) {
23 done := make(chan struct{})23 done := make(chan struct{})
24- state := ports.SessionStopped24+ state := ports.SessionDown
25 if record.DegradedReason != "" {25 if record.DegradedReason != "" {
26 state = ports.SessionBroken26 state = ports.SessionBroken
27 close(done)27 close(done)
@@ -179,7 +179,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
179 return nil179 return nil
180 }180 }
181 if record.Committed == nil {181 if record.Committed == nil {
182- d.setStoppedRecovery(record, ports.SessionStopped)182+ d.setStoppedRecovery(record, ports.SessionDown)
183 d.logSessionRestoreComplete(record, 0, false)183 d.logSessionRestoreComplete(record, 0, false)
184 return nil184 return nil
185 }185 }
@@ -231,7 +231,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
231 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {231 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {
232 return err232 return err
233 }233 }
234- d.setStoppedRecovery(record, ports.SessionStopped)234+ d.setStoppedRecovery(record, ports.SessionDown)
235 d.logSessionRestoreComplete(record, selected.Generation, false)235 d.logSessionRestoreComplete(record, selected.Generation, false)
236 return nil236 return nil
237 }237 }
@@ -252,7 +252,7 @@ func (d *Daemon) resetIncompatibleCheckpoint(ctx context.Context, record domain.
252 252
253 d.mu.Lock()253 d.mu.Lock()
254 if entry, ok := d.stopped[record.Name]; ok {254 if entry, ok := d.stopped[record.Name]; ok {
255- d.stopped[record.Name] = stoppedSessionFromRecord(fresh, ports.SessionStopped, entry.restoreDone)255+ d.stopped[record.Name] = stoppedSessionFromRecord(fresh, ports.SessionDown, entry.restoreDone)
256 }256 }
257 d.mu.Unlock()257 d.mu.Unlock()
258 258
M internal/usecase/picker/model.go +3 -3
@@ -596,8 +596,8 @@ func (r row) remoteKeyPointer() *domain.RemoteSessionKey {
596596 // renderList draws each visible row as up to three segments: a name segment
597597 // (bold when styles came from a truecolor theme), a base-styled attention
598598 // marker right after the name, and a muted detail segment (tab rows only) —
599-// or a base-styled "(stopped)" suffix for stopped session headers. A tight
600-// width ellipsizes the detail segment before eating into the name.
599+// or a base-styled "(down)" suffix for down session headers. A tight width
600+// ellipsizes the detail segment before eating into the name.
601601 func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles RenderStyles) {
602602 if m == nil || rect.Width <= 0 || rect.Height <= 0 {
603603 return
@@ -640,7 +640,7 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render
640640 } else {
641641 detail += " "
642642 }
643- ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(stopped)", base)
643+ ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(down)", base)
644644 } else {
645645 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)
646646 }
@@ -596,8 +596,8 @@ func (r row) remoteKeyPointer() *domain.RemoteSessionKey {
596 // renderList draws each visible row as up to three segments: a name segment596 // renderList draws each visible row as up to three segments: a name segment
597 // (bold when styles came from a truecolor theme), a base-styled attention597 // (bold when styles came from a truecolor theme), a base-styled attention
598 // marker right after the name, and a muted detail segment (tab rows only) —598 // marker right after the name, and a muted detail segment (tab rows only) —
599-// or a base-styled "(stopped)" suffix for stopped session headers. A tight599+// or a base-styled "(down)" suffix for down session headers. A tight width
600-// width ellipsizes the detail segment before eating into the name.600+// ellipsizes the detail segment before eating into the name.
601 func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles RenderStyles) {601 func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles RenderStyles) {
602 if m == nil || rect.Width <= 0 || rect.Height <= 0 {602 if m == nil || rect.Width <= 0 || rect.Height <= 0 {
603 return603 return
@@ -640,7 +640,7 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render
640 } else {640 } else {
641 detail += " "641 detail += " "
642 }642 }
643- ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(stopped)", base)643+ ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(down)", base)
644 } else {644 } else {
645 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)645 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)
646 }646 }
M internal/usecase/picker/model_test.go +8 -2
@@ -2,6 +2,7 @@ package picker
22
33 import (
44 "reflect"
5+ "strings"
56 "testing"
67
78 "github.com/bnema/vev/internal/domain"
@@ -498,6 +499,11 @@ func TestStoppedSessionSelectableAndRendered(t *testing.T) {
498499 frame := m.Render(domain.Size{Cols: 24, Rows: 4}, Preview{})
499500 require.Equal(t, 'w', frame.At(0, 0).Rune)
500501 require.Equal(t, '(', frame.At(5, 0).Rune)
502+ var label strings.Builder
503+ for _, cell := range frame.Row(0) {
504+ label.WriteRune(cell.Rune)
505+ }
506+ require.Contains(t, label.String(), "(down)")
501507 }
502508
503509 func TestRenderStopsStoppedRowsDimItalic(t *testing.T) {
@@ -510,7 +516,7 @@ func TestRenderStopsStoppedRowsDimItalic(t *testing.T) {
510516 require.Equal(t, stoppedStyle, styles.Stopped)
511517
512518 frame := m.Render(domain.Size{Cols: 15, Rows: 6}, Preview{})
513- // Rows: 0 "work" header, 1 " tab" (selected), 2 "old (stopped)" header, 3 its tab row.
519+ // Rows: 0 "work" header, 1 " tab" (selected), 2 "old (down)" header, 3 its tab row.
514520 require.Equal(t, stoppedStyle, frame.Row(2)[0].Style, "stopped header must be dim italic")
515521 require.Equal(t, stoppedStyle, frame.Row(3)[0].Style, "stopped tab row must be dim italic")
516522 require.NotEqual(t, stoppedStyle, frame.Row(0)[0].Style, "live header keeps base style")
@@ -587,7 +593,7 @@ func TestRenderListDrawsNameAndDetailSegmentsWithDistinctStyles(t *testing.T) {
587593 }
588594
589595 m := New([]SessionView{{ID: "s1", Name: "one", Tabs: []TabEntry{
590- {Name: "alpha", Detail: " (running)"},
596+ {Name: "alpha", Detail: " (up)"},
591597 {Name: "beta", Detail: " (idle)", Attention: true},
592598 }, Active: 0}}, SelectionConfig{Mode: SelectNavigationTab})
593599
@@ -2,6 +2,7 @@ package picker
2 2
3 import (3 import (
4 "reflect"4 "reflect"
5+ "strings"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
@@ -498,6 +499,11 @@ func TestStoppedSessionSelectableAndRendered(t *testing.T) {
498 frame := m.Render(domain.Size{Cols: 24, Rows: 4}, Preview{})499 frame := m.Render(domain.Size{Cols: 24, Rows: 4}, Preview{})
499 require.Equal(t, 'w', frame.At(0, 0).Rune)500 require.Equal(t, 'w', frame.At(0, 0).Rune)
500 require.Equal(t, '(', frame.At(5, 0).Rune)501 require.Equal(t, '(', frame.At(5, 0).Rune)
502+ var label strings.Builder
503+ for _, cell := range frame.Row(0) {
504+ label.WriteRune(cell.Rune)
505+ }
506+ require.Contains(t, label.String(), "(down)")
501 }507 }
502 508
503 func TestRenderStopsStoppedRowsDimItalic(t *testing.T) {509 func TestRenderStopsStoppedRowsDimItalic(t *testing.T) {
@@ -510,7 +516,7 @@ func TestRenderStopsStoppedRowsDimItalic(t *testing.T) {
510 require.Equal(t, stoppedStyle, styles.Stopped)516 require.Equal(t, stoppedStyle, styles.Stopped)
511 517
512 frame := m.Render(domain.Size{Cols: 15, Rows: 6}, Preview{})518 frame := m.Render(domain.Size{Cols: 15, Rows: 6}, Preview{})
513- // Rows: 0 "work" header, 1 " tab" (selected), 2 "old (stopped)" header, 3 its tab row.519+ // Rows: 0 "work" header, 1 " tab" (selected), 2 "old (down)" header, 3 its tab row.
514 require.Equal(t, stoppedStyle, frame.Row(2)[0].Style, "stopped header must be dim italic")520 require.Equal(t, stoppedStyle, frame.Row(2)[0].Style, "stopped header must be dim italic")
515 require.Equal(t, stoppedStyle, frame.Row(3)[0].Style, "stopped tab row must be dim italic")521 require.Equal(t, stoppedStyle, frame.Row(3)[0].Style, "stopped tab row must be dim italic")
516 require.NotEqual(t, stoppedStyle, frame.Row(0)[0].Style, "live header keeps base style")522 require.NotEqual(t, stoppedStyle, frame.Row(0)[0].Style, "live header keeps base style")
@@ -587,7 +593,7 @@ func TestRenderListDrawsNameAndDetailSegmentsWithDistinctStyles(t *testing.T) {
587 }593 }
588 594
589 m := New([]SessionView{{ID: "s1", Name: "one", Tabs: []TabEntry{595 m := New([]SessionView{{ID: "s1", Name: "one", Tabs: []TabEntry{
590- {Name: "alpha", Detail: " (running)"},596+ {Name: "alpha", Detail: " (up)"},
591 {Name: "beta", Detail: " (idle)", Attention: true},597 {Name: "beta", Detail: " (idle)", Attention: true},
592 }, Active: 0}}, SelectionConfig{Mode: SelectNavigationTab})598 }, Active: 0}}, SelectionConfig{Mode: SelectNavigationTab})
593 599
M scripts/remote-picker-harness/main.go +5 -5
@@ -94,7 +94,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
9494 if err := sendInputAndAwait(ctx, stdio, "printf 'VEV_REMOTE_HARNESS_ENV=%s:%s:%s:%s\\n' \"$VEV_TEST_ENV\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$VEV\"", "local:/run/local:wayland-local:"); err != nil {
9595 return fmt.Errorf("direct client environment: %w", err)
9696 }
97- live, err := waitForSession(ctx, catalog, target, sessionName, "running")
97+ live, err := waitForSession(ctx, catalog, target, sessionName, "up")
9898 if err != nil {
9999 return fmt.Errorf("live catalog: %w", err)
100100 }
@@ -108,7 +108,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
108108 if err := sendCommand(ctx, stdio, "new-tab"); err != nil {
109109 return fmt.Errorf("create second tab: %w", err)
110110 }
111- liveWithTabs, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "running", 2)
111+ liveWithTabs, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "up", 2)
112112 if err != nil {
113113 return fmt.Errorf("multi-tab catalog: %w", err)
114114 }
@@ -116,7 +116,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
116116 if err := sendCommand(ctx, stdio, "close-tab"); err != nil {
117117 return fmt.Errorf("remove tab: %w", err)
118118 }
119- after, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "running", 1)
119+ after, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "up", 1)
120120 if err != nil {
121121 return fmt.Errorf("removed-tab catalog: %w", err)
122122 }
@@ -192,7 +192,7 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
192192 if err := sendInputAndAwait(ctx, remote, "printf '"+previewMarker+"\\n'", previewMarker); err != nil {
193193 return fmt.Errorf("seed remote picker preview: %w", err)
194194 }
195- remoteCatalog, err := waitForSessionWithTabCount(ctx, catalog, target, remoteSession, "running", 2)
195+ remoteCatalog, err := waitForSessionWithTabCount(ctx, catalog, target, remoteSession, "up", 2)
196196 if err != nil {
197197 return fmt.Errorf("remote picker catalog: %w", err)
198198 }
@@ -335,7 +335,7 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem
335335 if err := runRemoteCommand(ctx, target, "vev", "--daemon-launcher"); err != nil {
336336 return domain.RemoteSessionTarget{}, fmt.Errorf("restart remote daemon: %w", err)
337337 }
338- resumedCatalog, err := waitForSession(ctx, catalog, target, sessionName, "running")
338+ resumedCatalog, err := waitForSession(ctx, catalog, target, sessionName, "up")
339339 if err != nil {
340340 return domain.RemoteSessionTarget{}, fmt.Errorf("resumed catalog: %w", err)
341341 }
@@ -94,7 +94,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
94 if err := sendInputAndAwait(ctx, stdio, "printf 'VEV_REMOTE_HARNESS_ENV=%s:%s:%s:%s\\n' \"$VEV_TEST_ENV\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$VEV\"", "local:/run/local:wayland-local:"); err != nil {94 if err := sendInputAndAwait(ctx, stdio, "printf 'VEV_REMOTE_HARNESS_ENV=%s:%s:%s:%s\\n' \"$VEV_TEST_ENV\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$VEV\"", "local:/run/local:wayland-local:"); err != nil {
95 return fmt.Errorf("direct client environment: %w", err)95 return fmt.Errorf("direct client environment: %w", err)
96 }96 }
97- live, err := waitForSession(ctx, catalog, target, sessionName, "running")97+ live, err := waitForSession(ctx, catalog, target, sessionName, "up")
98 if err != nil {98 if err != nil {
99 return fmt.Errorf("live catalog: %w", err)99 return fmt.Errorf("live catalog: %w", err)
100 }100 }
@@ -108,7 +108,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
108 if err := sendCommand(ctx, stdio, "new-tab"); err != nil {108 if err := sendCommand(ctx, stdio, "new-tab"); err != nil {
109 return fmt.Errorf("create second tab: %w", err)109 return fmt.Errorf("create second tab: %w", err)
110 }110 }
111- liveWithTabs, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "running", 2)111+ liveWithTabs, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "up", 2)
112 if err != nil {112 if err != nil {
113 return fmt.Errorf("multi-tab catalog: %w", err)113 return fmt.Errorf("multi-tab catalog: %w", err)
114 }114 }
@@ -116,7 +116,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
116 if err := sendCommand(ctx, stdio, "close-tab"); err != nil {116 if err := sendCommand(ctx, stdio, "close-tab"); err != nil {
117 return fmt.Errorf("remove tab: %w", err)117 return fmt.Errorf("remove tab: %w", err)
118 }118 }
119- after, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "running", 1)119+ after, err := waitForSessionWithTabCount(ctx, catalog, target, sessionName, "up", 1)
120 if err != nil {120 if err != nil {
121 return fmt.Errorf("removed-tab catalog: %w", err)121 return fmt.Errorf("removed-tab catalog: %w", err)
122 }122 }
@@ -192,7 +192,7 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
192 if err := sendInputAndAwait(ctx, remote, "printf '"+previewMarker+"\\n'", previewMarker); err != nil {192 if err := sendInputAndAwait(ctx, remote, "printf '"+previewMarker+"\\n'", previewMarker); err != nil {
193 return fmt.Errorf("seed remote picker preview: %w", err)193 return fmt.Errorf("seed remote picker preview: %w", err)
194 }194 }
195- remoteCatalog, err := waitForSessionWithTabCount(ctx, catalog, target, remoteSession, "running", 2)195+ remoteCatalog, err := waitForSessionWithTabCount(ctx, catalog, target, remoteSession, "up", 2)
196 if err != nil {196 if err != nil {
197 return fmt.Errorf("remote picker catalog: %w", err)197 return fmt.Errorf("remote picker catalog: %w", err)
198 }198 }
@@ -335,7 +335,7 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem
335 if err := runRemoteCommand(ctx, target, "vev", "--daemon-launcher"); err != nil {335 if err := runRemoteCommand(ctx, target, "vev", "--daemon-launcher"); err != nil {
336 return domain.RemoteSessionTarget{}, fmt.Errorf("restart remote daemon: %w", err)336 return domain.RemoteSessionTarget{}, fmt.Errorf("restart remote daemon: %w", err)
337 }337 }
338- resumedCatalog, err := waitForSession(ctx, catalog, target, sessionName, "running")338+ resumedCatalog, err := waitForSession(ctx, catalog, target, sessionName, "up")
339 if err != nil {339 if err != nil {
340 return domain.RemoteSessionTarget{}, fmt.Errorf("resumed catalog: %w", err)340 return domain.RemoteSessionTarget{}, fmt.Errorf("resumed catalog: %w", err)
341 }341 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close