detached b96d645

refactor(client): group picker session state

commit
b96d645fd0fa30761801a262272b335687dfe7bb
author
brice · · 2026-10-08 05:58 UTC
committer
brice · · 2026-10-08 08:12 UTC
parent
4992fe8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +57 -45
M internal/usecase/client/preview_manager_test.go +5 -5
@@ -342,15 +342,15 @@ func TestSupervisorRefreshPreviewWiresTerminalGeometryToPreviewManager(t *testin
342342 h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once()
343343 sub.EXPECT().Close().Once()
344344
345- supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, preview: previewManager{clock: h.clock}}
345+ supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, picker: pickerSession{preview: previewManager{clock: h.clock}}}
346346 supervisor.refreshPreview(h.service)
347347 h.clock.awaitTimer(t).fire()
348348 select {
349- case <-supervisor.preview.changed():
349+ case <-supervisor.picker.preview.changed():
350350 case <-time.After(5 * time.Second):
351351 t.Fatal("the supervisor preview did not wake")
352352 }
353- supervisor.preview.publish(h.controller)
354- require.Equal(t, expected, supervisor.preview.request)
355- supervisor.preview.close(h.controller)
353+ supervisor.picker.preview.publish(h.controller)
354+ require.Equal(t, expected, supervisor.picker.preview.request)
355+ supervisor.picker.preview.close(h.controller)
356356 }
@@ -342,15 +342,15 @@ func TestSupervisorRefreshPreviewWiresTerminalGeometryToPreviewManager(t *testin
342 h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once()342 h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once()
343 sub.EXPECT().Close().Once()343 sub.EXPECT().Close().Once()
344 344
345- supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, preview: previewManager{clock: h.clock}}345+ supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, picker: pickerSession{preview: previewManager{clock: h.clock}}}
346 supervisor.refreshPreview(h.service)346 supervisor.refreshPreview(h.service)
347 h.clock.awaitTimer(t).fire()347 h.clock.awaitTimer(t).fire()
348 select {348 select {
349- case <-supervisor.preview.changed():349+ case <-supervisor.picker.preview.changed():
350 case <-time.After(5 * time.Second):350 case <-time.After(5 * time.Second):
351 t.Fatal("the supervisor preview did not wake")351 t.Fatal("the supervisor preview did not wake")
352 }352 }
353- supervisor.preview.publish(h.controller)353+ supervisor.picker.preview.publish(h.controller)
354- require.Equal(t, expected, supervisor.preview.request)354+ require.Equal(t, expected, supervisor.picker.preview.request)
355- supervisor.preview.close(h.controller)355+ supervisor.picker.preview.close(h.controller)
356 }356 }
M internal/usecase/client/supervisor.go +15 -18
@@ -434,10 +434,9 @@ type Supervisor struct {
434434 cfg SupervisorConfig
435435 logger *slog.Logger
436436
437- // pendingPickerKey retains a commit observed while connecting. It is
438- // revalidated against the adopted service just like a ready-phase commit.
439- pendingPickerKey string
440- preview previewManager
437+ // picker groups the picker's per-run state: the pending commit key, the
438+ // preview subscription, and the off-goroutine kill runner.
439+ picker pickerSession
441440
442441 mu sync.Mutex
443442 state State
@@ -471,8 +470,6 @@ type Supervisor struct {
471470 // nav is the run goroutine's navigation bookkeeping: the ready-phase
472471 // subscription, pending swap and in-place choices, and the route ledger.
473472 nav navigationState
474- // kills runs the picker's `x` operations off the run goroutine.
475- kills pickerKills
476473 }
477474
478475 // NewSupervisor validates the required dependencies and returns a supervisor
@@ -520,7 +517,7 @@ func NewSupervisor(cfg SupervisorConfig) (*Supervisor, error) {
520517 state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected},
521518 clientID: newClientID(),
522519 navigation: cfg.InitialNavigation,
523- preview: previewManager{clock: cfg.Clock},
520+ picker: pickerSession{preview: previewManager{clock: cfg.Clock}},
524521 }
525522 // The host is the supervisor's existing foreground grant. It owns no raw
526523 // mode and starts no reader: the supervisor keeps its one terminal input
@@ -592,7 +589,7 @@ func (s *Supervisor) Run(ctx context.Context) (retErr error) {
592589 )
593590 retire := func() {
594591 s.retirePickerKill()
595- s.preview.close(s.cfg.Picker)
592+ s.picker.preview.close(s.cfg.Picker)
596593 s.nav.clearReady()
597594 if sub != nil {
598595 sub.Close()
@@ -898,8 +895,8 @@ type readyOutcome struct {
898895 // the supervisor can admit exactly the row the user committed. It opens no
899896 // stream and starts no terminal work itself.
900897 func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome {
901- if key := s.pendingPickerKey; key != "" {
902- s.pendingPickerKey = ""
898+ if key := s.picker.pendingPickerKey; key != "" {
899+ s.picker.pendingPickerKey = ""
903900 return readyOutcome{commitKey: key}
904901 }
905902 var changed <-chan struct{}
@@ -922,11 +919,11 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
922919 s.cfg.Picker.ApplySnapshot(service.Snapshot())
923920 s.refreshPreview(service)
924921 s.renderCurrent()
925- case <-s.preview.changed():
926- if s.preview.publish(s.cfg.Picker) {
922+ case <-s.picker.preview.changed():
923+ if s.picker.preview.publish(s.cfg.Picker) {
927924 s.renderCurrent()
928925 }
929- case outcome := <-s.kills.results():
926+ case outcome := <-s.picker.kills.results():
930927 s.finishPickerKill(service, outcome)
931928 case <-ops:
932929 op, key := s.cfg.Picker.TakeOp()
@@ -944,7 +941,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
944941 if op.exit {
945942 // Ctrl+C is the explicit exit. With no attachment to
946943 // return to, it is the only close that ends the process.
947- s.preview.close(s.cfg.Picker)
944+ s.picker.preview.close(s.cfg.Picker)
948945 return readyOutcome{terminated: true}
949946 }
950947 // Escape and q cancel; with no attachment there is nothing to
@@ -954,7 +951,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
954951 continue
955952 }
956953 if op.commit && key != "" {
957- s.preview.close(s.cfg.Picker)
954+ s.picker.preview.close(s.cfg.Picker)
958955 return readyOutcome{commitKey: key}
959956 }
960957 case <-ctx.Done():
@@ -974,10 +971,10 @@ func (s *Supervisor) refreshPreview(service ports.BrokerNavigator) {
974971 }
975972 geometry, err := s.cfg.Terminal.Geometry()
976973 if err != nil || !geometry.Valid() {
977- s.preview.close(s.cfg.Picker)
974+ s.picker.preview.close(s.cfg.Picker)
978975 return
979976 }
980- s.preview.refresh(service, s.cfg.Picker, geometry.Size)
977+ s.picker.preview.refresh(service, s.cfg.Picker, geometry.Size)
981978 }
982979
983980 // pickerOps is nil when no picker owns presentation decisions.
@@ -994,7 +991,7 @@ func (s *Supervisor) pickerOps() <-chan struct{} {
994991 func (s *Supervisor) takePickerClose() bool {
995992 op, key := s.cfg.Picker.TakeOp()
996993 if op.commit && key != "" {
997- s.pendingPickerKey = key
994+ s.picker.pendingPickerKey = key
998995 }
999996 if op.close && !op.exit {
1000997 s.notifyPicker(pickerExitHint)
@@ -434,10 +434,9 @@ type Supervisor struct {
434 cfg SupervisorConfig434 cfg SupervisorConfig
435 logger *slog.Logger435 logger *slog.Logger
436 436
437- // pendingPickerKey retains a commit observed while connecting. It is437+ // picker groups the picker's per-run state: the pending commit key, the
438- // revalidated against the adopted service just like a ready-phase commit.438+ // preview subscription, and the off-goroutine kill runner.
439- pendingPickerKey string439+ picker pickerSession
440- preview previewManager
441 440
442 mu sync.Mutex441 mu sync.Mutex
443 state State442 state State
@@ -471,8 +470,6 @@ type Supervisor struct {
471 // nav is the run goroutine's navigation bookkeeping: the ready-phase470 // nav is the run goroutine's navigation bookkeeping: the ready-phase
472 // subscription, pending swap and in-place choices, and the route ledger.471 // subscription, pending swap and in-place choices, and the route ledger.
473 nav navigationState472 nav navigationState
474- // kills runs the picker's `x` operations off the run goroutine.
475- kills pickerKills
476 }473 }
477 474
478 // NewSupervisor validates the required dependencies and returns a supervisor475 // NewSupervisor validates the required dependencies and returns a supervisor
@@ -520,7 +517,7 @@ func NewSupervisor(cfg SupervisorConfig) (*Supervisor, error) {
520 state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected},517 state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected},
521 clientID: newClientID(),518 clientID: newClientID(),
522 navigation: cfg.InitialNavigation,519 navigation: cfg.InitialNavigation,
523- preview: previewManager{clock: cfg.Clock},520+ picker: pickerSession{preview: previewManager{clock: cfg.Clock}},
524 }521 }
525 // The host is the supervisor's existing foreground grant. It owns no raw522 // The host is the supervisor's existing foreground grant. It owns no raw
526 // mode and starts no reader: the supervisor keeps its one terminal input523 // mode and starts no reader: the supervisor keeps its one terminal input
@@ -592,7 +589,7 @@ func (s *Supervisor) Run(ctx context.Context) (retErr error) {
592 )589 )
593 retire := func() {590 retire := func() {
594 s.retirePickerKill()591 s.retirePickerKill()
595- s.preview.close(s.cfg.Picker)592+ s.picker.preview.close(s.cfg.Picker)
596 s.nav.clearReady()593 s.nav.clearReady()
597 if sub != nil {594 if sub != nil {
598 sub.Close()595 sub.Close()
@@ -898,8 +895,8 @@ type readyOutcome struct {
898 // the supervisor can admit exactly the row the user committed. It opens no895 // the supervisor can admit exactly the row the user committed. It opens no
899 // stream and starts no terminal work itself.896 // stream and starts no terminal work itself.
900 func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome {897 func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome {
901- if key := s.pendingPickerKey; key != "" {898+ if key := s.picker.pendingPickerKey; key != "" {
902- s.pendingPickerKey = ""899+ s.picker.pendingPickerKey = ""
903 return readyOutcome{commitKey: key}900 return readyOutcome{commitKey: key}
904 }901 }
905 var changed <-chan struct{}902 var changed <-chan struct{}
@@ -922,11 +919,11 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
922 s.cfg.Picker.ApplySnapshot(service.Snapshot())919 s.cfg.Picker.ApplySnapshot(service.Snapshot())
923 s.refreshPreview(service)920 s.refreshPreview(service)
924 s.renderCurrent()921 s.renderCurrent()
925- case <-s.preview.changed():922+ case <-s.picker.preview.changed():
926- if s.preview.publish(s.cfg.Picker) {923+ if s.picker.preview.publish(s.cfg.Picker) {
927 s.renderCurrent()924 s.renderCurrent()
928 }925 }
929- case outcome := <-s.kills.results():926+ case outcome := <-s.picker.kills.results():
930 s.finishPickerKill(service, outcome)927 s.finishPickerKill(service, outcome)
931 case <-ops:928 case <-ops:
932 op, key := s.cfg.Picker.TakeOp()929 op, key := s.cfg.Picker.TakeOp()
@@ -944,7 +941,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
944 if op.exit {941 if op.exit {
945 // Ctrl+C is the explicit exit. With no attachment to942 // Ctrl+C is the explicit exit. With no attachment to
946 // return to, it is the only close that ends the process.943 // return to, it is the only close that ends the process.
947- s.preview.close(s.cfg.Picker)944+ s.picker.preview.close(s.cfg.Picker)
948 return readyOutcome{terminated: true}945 return readyOutcome{terminated: true}
949 }946 }
950 // Escape and q cancel; with no attachment there is nothing to947 // Escape and q cancel; with no attachment there is nothing to
@@ -954,7 +951,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim
954 continue951 continue
955 }952 }
956 if op.commit && key != "" {953 if op.commit && key != "" {
957- s.preview.close(s.cfg.Picker)954+ s.picker.preview.close(s.cfg.Picker)
958 return readyOutcome{commitKey: key}955 return readyOutcome{commitKey: key}
959 }956 }
960 case <-ctx.Done():957 case <-ctx.Done():
@@ -974,10 +971,10 @@ func (s *Supervisor) refreshPreview(service ports.BrokerNavigator) {
974 }971 }
975 geometry, err := s.cfg.Terminal.Geometry()972 geometry, err := s.cfg.Terminal.Geometry()
976 if err != nil || !geometry.Valid() {973 if err != nil || !geometry.Valid() {
977- s.preview.close(s.cfg.Picker)974+ s.picker.preview.close(s.cfg.Picker)
978 return975 return
979 }976 }
980- s.preview.refresh(service, s.cfg.Picker, geometry.Size)977+ s.picker.preview.refresh(service, s.cfg.Picker, geometry.Size)
981 }978 }
982 979
983 // pickerOps is nil when no picker owns presentation decisions.980 // pickerOps is nil when no picker owns presentation decisions.
@@ -994,7 +991,7 @@ func (s *Supervisor) pickerOps() <-chan struct{} {
994 func (s *Supervisor) takePickerClose() bool {991 func (s *Supervisor) takePickerClose() bool {
995 op, key := s.cfg.Picker.TakeOp()992 op, key := s.cfg.Picker.TakeOp()
996 if op.commit && key != "" {993 if op.commit && key != "" {
997- s.pendingPickerKey = key994+ s.picker.pendingPickerKey = key
998 }995 }
999 if op.close && !op.exit {996 if op.close && !op.exit {
1000 s.notifyPicker(pickerExitHint)997 s.notifyPicker(pickerExitHint)
M internal/usecase/client/supervisor_attach.go +2 -2
@@ -551,7 +551,7 @@ func (s *Supervisor) runInitialNavigation(ctx context.Context, input *terminalIn
551551 if s.cfg.Picker != nil {
552552 s.cfg.Picker.TakeOp()
553553 }
554- s.pendingPickerKey = ""
554+ s.picker.pendingPickerKey = ""
555555 return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI)
556556 }
557557
@@ -901,7 +901,7 @@ settlement:
901901 overlay.resize()
902902 case <-notices.arm(s.cfg.Picker):
903903 s.renderCurrent()
904- case outcome := <-s.kills.results():
904+ case outcome := <-s.picker.kills.results():
905905 s.finishPickerKill(service, outcome)
906906 case <-service.Done():
907907 brokerLost = true
@@ -551,7 +551,7 @@ func (s *Supervisor) runInitialNavigation(ctx context.Context, input *terminalIn
551 if s.cfg.Picker != nil {551 if s.cfg.Picker != nil {
552 s.cfg.Picker.TakeOp()552 s.cfg.Picker.TakeOp()
553 }553 }
554- s.pendingPickerKey = ""554+ s.picker.pendingPickerKey = ""
555 return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI)555 return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI)
556 }556 }
557 557
@@ -901,7 +901,7 @@ settlement:
901 overlay.resize()901 overlay.resize()
902 case <-notices.arm(s.cfg.Picker):902 case <-notices.arm(s.cfg.Picker):
903 s.renderCurrent()903 s.renderCurrent()
904- case outcome := <-s.kills.results():904+ case outcome := <-s.picker.kills.results():
905 s.finishPickerKill(service, outcome)905 s.finishPickerKill(service, outcome)
906 case <-service.Done():906 case <-service.Done():
907 brokerLost = true907 brokerLost = true
M internal/usecase/client/supervisor_overlay.go +4 -4
@@ -44,7 +44,7 @@ func (o *attachmentPickerOverlay) previewChanged() <-chan struct{} {
4444 if !o.active {
4545 return nil
4646 }
47- return o.sup.preview.changed()
47+ return o.sup.picker.preview.changed()
4848 }
4949
5050 func (o *attachmentPickerOverlay) ops() <-chan struct{} {
@@ -113,7 +113,7 @@ func (o *attachmentPickerOverlay) drop() {
113113 return
114114 }
115115 o.active = false
116- o.sup.preview.close(o.picker())
116+ o.sup.picker.preview.close(o.picker())
117117 o.picker().SetOwnsInput(false)
118118 }
119119
@@ -124,7 +124,7 @@ func (o *attachmentPickerOverlay) applyPublication() {
124124 }
125125
126126 func (o *attachmentPickerOverlay) publishPreview() {
127- if o.sup.preview.publish(o.picker()) {
127+ if o.sup.picker.preview.publish(o.picker()) {
128128 o.sup.renderCurrent()
129129 }
130130 }
@@ -181,7 +181,7 @@ func (o *attachmentPickerOverlay) takeOp() {
181181 }
182182 s.nav.setSwap(&target)
183183 o.swapping = true
184- s.preview.close(o.picker())
184+ s.picker.preview.close(o.picker())
185185 s.attachments.requestDetach(o.run.token)
186186 case op.kill && key != "":
187187 s.startPickerKill(o.service, key)
@@ -44,7 +44,7 @@ func (o *attachmentPickerOverlay) previewChanged() <-chan struct{} {
44 if !o.active {44 if !o.active {
45 return nil45 return nil
46 }46 }
47- return o.sup.preview.changed()47+ return o.sup.picker.preview.changed()
48 }48 }
49 49
50 func (o *attachmentPickerOverlay) ops() <-chan struct{} {50 func (o *attachmentPickerOverlay) ops() <-chan struct{} {
@@ -113,7 +113,7 @@ func (o *attachmentPickerOverlay) drop() {
113 return113 return
114 }114 }
115 o.active = false115 o.active = false
116- o.sup.preview.close(o.picker())116+ o.sup.picker.preview.close(o.picker())
117 o.picker().SetOwnsInput(false)117 o.picker().SetOwnsInput(false)
118 }118 }
119 119
@@ -124,7 +124,7 @@ func (o *attachmentPickerOverlay) applyPublication() {
124 }124 }
125 125
126 func (o *attachmentPickerOverlay) publishPreview() {126 func (o *attachmentPickerOverlay) publishPreview() {
127- if o.sup.preview.publish(o.picker()) {127+ if o.sup.picker.preview.publish(o.picker()) {
128 o.sup.renderCurrent()128 o.sup.renderCurrent()
129 }129 }
130 }130 }
@@ -181,7 +181,7 @@ func (o *attachmentPickerOverlay) takeOp() {
181 }181 }
182 s.nav.setSwap(&target)182 s.nav.setSwap(&target)
183 o.swapping = true183 o.swapping = true
184- s.preview.close(o.picker())184+ s.picker.preview.close(o.picker())
185 s.attachments.requestDetach(o.run.token)185 s.attachments.requestDetach(o.run.token)
186 case op.kill && key != "":186 case op.kill && key != "":
187 s.startPickerKill(o.service, key)187 s.startPickerKill(o.service, key)
M internal/usecase/client/supervisor_picker_kill.go +16 -16
@@ -69,7 +69,7 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string)
6969 if !ok || key == "" || supervisorNil(service) {
7070 return
7171 }
72- if s.kills.running {
72+ if s.picker.kills.running {
7373 s.notifyPicker(pickerKillRefused("a kill is already in progress"))
7474 return
7575 }
@@ -82,17 +82,17 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string)
8282 s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable"))
8383 return
8484 }
85- if s.kills.done == nil {
86- s.kills.done = make(chan pickerKillOutcome, 1)
85+ if s.picker.kills.done == nil {
86+ s.picker.kills.done = make(chan pickerKillOutcome, 1)
8787 }
8888 killCtx, cancel := context.WithCancel(context.Background())
89- s.kills.running = true
90- s.kills.cancel = cancel
91- s.kills.wg.Add(1)
89+ s.picker.kills.running = true
90+ s.picker.kills.cancel = cancel
91+ s.picker.kills.wg.Add(1)
9292 go func() {
93- defer s.kills.wg.Done()
93+ defer s.picker.kills.wg.Done()
9494 result, err := operations.Kill(killCtx, target.route, target.name)
95- s.kills.done <- pickerKillOutcome{target: target, result: result, err: err}
95+ s.picker.kills.done <- pickerKillOutcome{target: target, result: result, err: err}
9696 }()
9797 }
9898
@@ -110,22 +110,22 @@ func (s *Supervisor) finishPickerKill(service ports.BrokerNavigator, outcome pic
110110 // retirePickerKill cancels and joins an in-flight kill before its borrowed
111111 // service closes. Its outcome is still reported, never replayed.
112112 func (s *Supervisor) retirePickerKill() {
113- if !s.kills.running {
113+ if !s.picker.kills.running {
114114 return
115115 }
116- s.kills.cancel()
117- s.kills.wg.Wait()
118- outcome := <-s.kills.done
116+ s.picker.kills.cancel()
117+ s.picker.kills.wg.Wait()
118+ outcome := <-s.picker.kills.done
119119 s.settlePickerKill()
120120 s.notifyPicker(pickerKillNotice(outcome))
121121 }
122122
123123 func (s *Supervisor) settlePickerKill() {
124- if s.kills.cancel != nil {
125- s.kills.cancel()
124+ if s.picker.kills.cancel != nil {
125+ s.picker.kills.cancel()
126126 }
127- s.kills.running = false
128- s.kills.cancel = nil
127+ s.picker.kills.running = false
128+ s.picker.kills.cancel = nil
129129 }
130130
131131 // pickerKillRefused is a kill that was never sent.
@@ -69,7 +69,7 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string)
69 if !ok || key == "" || supervisorNil(service) {69 if !ok || key == "" || supervisorNil(service) {
70 return70 return
71 }71 }
72- if s.kills.running {72+ if s.picker.kills.running {
73 s.notifyPicker(pickerKillRefused("a kill is already in progress"))73 s.notifyPicker(pickerKillRefused("a kill is already in progress"))
74 return74 return
75 }75 }
@@ -82,17 +82,17 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string)
82 s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable"))82 s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable"))
83 return83 return
84 }84 }
85- if s.kills.done == nil {85+ if s.picker.kills.done == nil {
86- s.kills.done = make(chan pickerKillOutcome, 1)86+ s.picker.kills.done = make(chan pickerKillOutcome, 1)
87 }87 }
88 killCtx, cancel := context.WithCancel(context.Background())88 killCtx, cancel := context.WithCancel(context.Background())
89- s.kills.running = true89+ s.picker.kills.running = true
90- s.kills.cancel = cancel90+ s.picker.kills.cancel = cancel
91- s.kills.wg.Add(1)91+ s.picker.kills.wg.Add(1)
92 go func() {92 go func() {
93- defer s.kills.wg.Done()93+ defer s.picker.kills.wg.Done()
94 result, err := operations.Kill(killCtx, target.route, target.name)94 result, err := operations.Kill(killCtx, target.route, target.name)
95- s.kills.done <- pickerKillOutcome{target: target, result: result, err: err}95+ s.picker.kills.done <- pickerKillOutcome{target: target, result: result, err: err}
96 }()96 }()
97 }97 }
98 98
@@ -110,22 +110,22 @@ func (s *Supervisor) finishPickerKill(service ports.BrokerNavigator, outcome pic
110 // retirePickerKill cancels and joins an in-flight kill before its borrowed110 // retirePickerKill cancels and joins an in-flight kill before its borrowed
111 // service closes. Its outcome is still reported, never replayed.111 // service closes. Its outcome is still reported, never replayed.
112 func (s *Supervisor) retirePickerKill() {112 func (s *Supervisor) retirePickerKill() {
113- if !s.kills.running {113+ if !s.picker.kills.running {
114 return114 return
115 }115 }
116- s.kills.cancel()116+ s.picker.kills.cancel()
117- s.kills.wg.Wait()117+ s.picker.kills.wg.Wait()
118- outcome := <-s.kills.done118+ outcome := <-s.picker.kills.done
119 s.settlePickerKill()119 s.settlePickerKill()
120 s.notifyPicker(pickerKillNotice(outcome))120 s.notifyPicker(pickerKillNotice(outcome))
121 }121 }
122 122
123 func (s *Supervisor) settlePickerKill() {123 func (s *Supervisor) settlePickerKill() {
124- if s.kills.cancel != nil {124+ if s.picker.kills.cancel != nil {
125- s.kills.cancel()125+ s.picker.kills.cancel()
126 }126 }
127- s.kills.running = false127+ s.picker.kills.running = false
128- s.kills.cancel = nil128+ s.picker.kills.cancel = nil
129 }129 }
130 130
131 // pickerKillRefused is a kill that was never sent.131 // pickerKillRefused is a kill that was never sent.
A internal/usecase/client/supervisor_picker_session.go +15 -0
@@ -0,0 +1,15 @@
1+package client
2+
3+// pickerSession groups the picker state the supervisor carries across its run:
4+// the commit observed while connecting, the selected row's preview
5+// subscription, and the runner for the picker's kill operations. Like the
6+// fields it owns, it is only touched from the run goroutine. The zero value is
7+// ready once preview has its clock.
8+type pickerSession struct {
9+ // pendingPickerKey retains a commit observed while connecting. It is
10+ // revalidated against the adopted service just like a ready-phase commit.
11+ pendingPickerKey string
12+ preview previewManager
13+ // kills runs the picker's `x` operations off the run goroutine.
14+ kills pickerKills
15+}
@@ -0,0 +1,15 @@
1+package client
2+
3+// pickerSession groups the picker state the supervisor carries across its run:
4+// the commit observed while connecting, the selected row's preview
5+// subscription, and the runner for the picker's kill operations. Like the
6+// fields it owns, it is only touched from the run goroutine. The zero value is
7+// ready once preview has its clock.
8+type pickerSession struct {
9+ // pendingPickerKey retains a commit observed while connecting. It is
10+ // revalidated against the adopted service just like a ready-phase commit.
11+ pendingPickerKey string
12+ preview previewManager
13+ // kills runs the picker's `x` operations off the run goroutine.
14+ kills pickerKills
15+}

Keyboard shortcuts

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

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

Branches 12

Tags 11

↑↓ moveenter openesc close