detached 30c3dce

refactor(daemon): remove global active tab state

commit
30c3dce9a1407d8a2af7132346cd5e331ee0982e
author
brice · · 2026-08-03 01:09 UTC
committer
brice · · 2026-08-03 20:53 UTC
parent
409ae78
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
80 files · +579 -494
M internal/usecase/daemon/attachment_registry.go +37 -26
@@ -404,12 +404,10 @@ func (s *session) tabIndexForAttachment(ac *attachedClient) (int, int) {
404404 return 0, len(tabs)
405405 }
406406
407-func (s *session) tabForAttachmentOrActive(ac *attachedClient) *tab {
408- if tb := s.tabForAttachment(ac); tb != nil {
409- return tb
410- }
411- // Headless/session-wide callers have no attachment view. Use the first
412- // shared tab rather than a session-wide active-tab singleton.
407+// firstTab returns the first tab in the session's ordered tab collection.
408+// It is a deterministic session-level default for headless work, never an
409+// interactive attachment selection.
410+func (s *session) firstTab() *tab {
413411 if s == nil {
414412 return nil
415413 }
@@ -478,6 +476,20 @@ func (s *session) activateAttachmentViewLocked(ac *attachedClient, idx int) bool
478476 return true
479477 }
480478
479+func (s *session) selectAttachmentTabLocked(ac *attachedClient, tabID domain.TabStableID) bool {
480+ if s == nil || ac == nil || !attachmentRegisteredLocked(s, ac) {
481+ return false
482+ }
483+ view := ac.viewSnapshot()
484+ view.tabID = tabID
485+ view.paneID = ""
486+ view = s.repairAttachmentViewLocked(ac, view)
487+ view.revision++
488+ ac.publishView(view)
489+ s.invalidateViewsLocked()
490+ return view.tabID == tabID
491+}
492+
481493 func (s *session) selectAttachmentTab(ac *attachedClient, tabID domain.TabStableID) bool {
482494 if s == nil || ac == nil {
483495 return false
@@ -486,28 +498,22 @@ func (s *session) selectAttachmentTab(ac *attachedClient, tabID domain.TabStable
486498 _ = s.runMutation(func() error {
487499 s.mu.Lock()
488500 defer s.mu.Unlock()
489- if _, ok := s.attachments[ac]; !ok {
490- return nil
491- }
492- view := ac.viewSnapshot()
493- view.tabID = tabID
494- view.paneID = ""
495- view = s.repairAttachmentViewLocked(ac, view)
496- view.revision++
497- ac.publishView(view)
498- changed = view.tabID == tabID
499- s.invalidateViewsLocked()
501+ changed = s.selectAttachmentTabLocked(ac, tabID)
500502 return nil
501503 })
502504 return changed
503505 }
504506
505-func (s *session) switchAttachmentRelative(ac *attachedClient, delta int) bool {
506- position, count := s.tabIndexForAttachment(ac)
507- if count < 2 {
507+func (s *session) switchAttachmentTabForDispatch(ac *attachedClient, idx int) bool {
508+ if s == nil || ac == nil {
509+ return false
510+ }
511+ s.mu.Lock()
512+ defer s.mu.Unlock()
513+ if idx < 0 || idx >= len(s.tabs) {
508514 return false
509515 }
510- return s.switchAttachmentTab(ac, (position+delta+count)%count)
516+ return s.selectAttachmentTabLocked(ac, domain.TabStableID(s.tabs[idx].stableID))
511517 }
512518
513519 func (s *session) switchAttachmentTab(ac *attachedClient, idx int) bool {
@@ -515,17 +521,22 @@ func (s *session) switchAttachmentTab(ac *attachedClient, idx int) bool {
515521 return false
516522 }
517523 s.mu.Lock()
518- registered := false
519- if s.attachments != nil {
520- _, registered = s.attachments[ac]
521- }
524+ registered := attachmentRegisteredLocked(s, ac)
522525 if registered && idx >= 0 && idx < len(s.tabs) {
523526 tabID := domain.TabStableID(s.tabs[idx].stableID)
524527 s.mu.Unlock()
525528 return s.selectAttachmentTab(ac, tabID)
526529 }
527530 s.mu.Unlock()
528- return s.switchTab(idx)
531+ return false
532+}
533+
534+func (s *session) switchAttachmentRelativeForDispatch(ac *attachedClient, delta int) bool {
535+ position, count := s.tabIndexForAttachment(ac)
536+ if count < 2 {
537+ return false
538+ }
539+ return s.switchAttachmentTabForDispatch(ac, (position+delta+count)%count)
529540 }
530541
531542 // repairAttachmentViews repairs all live attachment targets after a shared
@@ -404,12 +404,10 @@ func (s *session) tabIndexForAttachment(ac *attachedClient) (int, int) {
404 return 0, len(tabs)404 return 0, len(tabs)
405 }405 }
406 406
407-func (s *session) tabForAttachmentOrActive(ac *attachedClient) *tab {407+// firstTab returns the first tab in the session's ordered tab collection.
408- if tb := s.tabForAttachment(ac); tb != nil {408+// It is a deterministic session-level default for headless work, never an
409- return tb409+// interactive attachment selection.
410- }410+func (s *session) firstTab() *tab {
411- // Headless/session-wide callers have no attachment view. Use the first
412- // shared tab rather than a session-wide active-tab singleton.
413 if s == nil {411 if s == nil {
414 return nil412 return nil
415 }413 }
@@ -478,6 +476,20 @@ func (s *session) activateAttachmentViewLocked(ac *attachedClient, idx int) bool
478 return true476 return true
479 }477 }
480 478
479+func (s *session) selectAttachmentTabLocked(ac *attachedClient, tabID domain.TabStableID) bool {
480+ if s == nil || ac == nil || !attachmentRegisteredLocked(s, ac) {
481+ return false
482+ }
483+ view := ac.viewSnapshot()
484+ view.tabID = tabID
485+ view.paneID = ""
486+ view = s.repairAttachmentViewLocked(ac, view)
487+ view.revision++
488+ ac.publishView(view)
489+ s.invalidateViewsLocked()
490+ return view.tabID == tabID
491+}
492+
481 func (s *session) selectAttachmentTab(ac *attachedClient, tabID domain.TabStableID) bool {493 func (s *session) selectAttachmentTab(ac *attachedClient, tabID domain.TabStableID) bool {
482 if s == nil || ac == nil {494 if s == nil || ac == nil {
483 return false495 return false
@@ -486,28 +498,22 @@ func (s *session) selectAttachmentTab(ac *attachedClient, tabID domain.TabStable
486 _ = s.runMutation(func() error {498 _ = s.runMutation(func() error {
487 s.mu.Lock()499 s.mu.Lock()
488 defer s.mu.Unlock()500 defer s.mu.Unlock()
489- if _, ok := s.attachments[ac]; !ok {501+ changed = s.selectAttachmentTabLocked(ac, tabID)
490- return nil
491- }
492- view := ac.viewSnapshot()
493- view.tabID = tabID
494- view.paneID = ""
495- view = s.repairAttachmentViewLocked(ac, view)
496- view.revision++
497- ac.publishView(view)
498- changed = view.tabID == tabID
499- s.invalidateViewsLocked()
500 return nil502 return nil
501 })503 })
502 return changed504 return changed
503 }505 }
504 506
505-func (s *session) switchAttachmentRelative(ac *attachedClient, delta int) bool {507+func (s *session) switchAttachmentTabForDispatch(ac *attachedClient, idx int) bool {
506- position, count := s.tabIndexForAttachment(ac)508+ if s == nil || ac == nil {
507- if count < 2 {509+ return false
510+ }
511+ s.mu.Lock()
512+ defer s.mu.Unlock()
513+ if idx < 0 || idx >= len(s.tabs) {
508 return false514 return false
509 }515 }
510- return s.switchAttachmentTab(ac, (position+delta+count)%count)516+ return s.selectAttachmentTabLocked(ac, domain.TabStableID(s.tabs[idx].stableID))
511 }517 }
512 518
513 func (s *session) switchAttachmentTab(ac *attachedClient, idx int) bool {519 func (s *session) switchAttachmentTab(ac *attachedClient, idx int) bool {
@@ -515,17 +521,22 @@ func (s *session) switchAttachmentTab(ac *attachedClient, idx int) bool {
515 return false521 return false
516 }522 }
517 s.mu.Lock()523 s.mu.Lock()
518- registered := false524+ registered := attachmentRegisteredLocked(s, ac)
519- if s.attachments != nil {
520- _, registered = s.attachments[ac]
521- }
522 if registered && idx >= 0 && idx < len(s.tabs) {525 if registered && idx >= 0 && idx < len(s.tabs) {
523 tabID := domain.TabStableID(s.tabs[idx].stableID)526 tabID := domain.TabStableID(s.tabs[idx].stableID)
524 s.mu.Unlock()527 s.mu.Unlock()
525 return s.selectAttachmentTab(ac, tabID)528 return s.selectAttachmentTab(ac, tabID)
526 }529 }
527 s.mu.Unlock()530 s.mu.Unlock()
528- return s.switchTab(idx)531+ return false
532+}
533+
534+func (s *session) switchAttachmentRelativeForDispatch(ac *attachedClient, delta int) bool {
535+ position, count := s.tabIndexForAttachment(ac)
536+ if count < 2 {
537+ return false
538+ }
539+ return s.switchAttachmentTabForDispatch(ac, (position+delta+count)%count)
529 }540 }
530 541
531 // repairAttachmentViews repairs all live attachment targets after a shared542 // repairAttachmentViews repairs all live attachment targets after a shared
M internal/usecase/daemon/attachment_transition_prelocked_test.go +2 -2
@@ -13,7 +13,7 @@ func TestValidateAttachmentTransitionPrelockedLeavesMembershipUntouchedOnFailure
1313
1414 d := newTestDaemon(t, nil, stubClock{})
1515 source := &session{sessionCore: sessionCore{id: domain.SessionID("source")}}
16- target := &session{sessionCore: sessionCore{id: domain.SessionID("target")}, active: 0}
16+ target := &session{sessionCore: sessionCore{id: domain.SessionID("target")}}
1717 next := &attachedClient{tr: &closeTrackingTransport{}}
1818 next.setSession(source)
1919 source.registerAttachmentLocked(next)
@@ -58,5 +58,5 @@ func TestValidateAttachmentTransitionPrelockedLeavesMembershipUntouchedOnFailure
5858 require.Same(t, source, next.currentSession())
5959 require.Equal(t, uint64(0), next.roleGeneration.Load())
6060 require.Equal(t, uint64(0), old.roleGeneration.Load())
61- require.Equal(t, 0, target.active)
61+ require.Equal(t, 0, testAttachmentTabIndex(target))
6262 }
@@ -13,7 +13,7 @@ func TestValidateAttachmentTransitionPrelockedLeavesMembershipUntouchedOnFailure
13 13
14 d := newTestDaemon(t, nil, stubClock{})14 d := newTestDaemon(t, nil, stubClock{})
15 source := &session{sessionCore: sessionCore{id: domain.SessionID("source")}}15 source := &session{sessionCore: sessionCore{id: domain.SessionID("source")}}
16- target := &session{sessionCore: sessionCore{id: domain.SessionID("target")}, active: 0}16+ target := &session{sessionCore: sessionCore{id: domain.SessionID("target")}}
17 next := &attachedClient{tr: &closeTrackingTransport{}}17 next := &attachedClient{tr: &closeTrackingTransport{}}
18 next.setSession(source)18 next.setSession(source)
19 source.registerAttachmentLocked(next)19 source.registerAttachmentLocked(next)
@@ -58,5 +58,5 @@ func TestValidateAttachmentTransitionPrelockedLeavesMembershipUntouchedOnFailure
58 require.Same(t, source, next.currentSession())58 require.Same(t, source, next.currentSession())
59 require.Equal(t, uint64(0), next.roleGeneration.Load())59 require.Equal(t, uint64(0), next.roleGeneration.Load())
60 require.Equal(t, uint64(0), old.roleGeneration.Load())60 require.Equal(t, uint64(0), old.roleGeneration.Load())
61- require.Equal(t, 0, target.active)61+ require.Equal(t, 0, testAttachmentTabIndex(target))
62 }62 }
M internal/usecase/daemon/attachment_transition_publish.go +1 -1
@@ -165,7 +165,7 @@ func (d *Daemon) validateAttachmentTransitionPrelocked(req attachmentTransitionR
165165 invalid = invalid || req.sourceToken == nil && source != req.target && req.expectedRole == attachmentActive && sourceCoordinator != nil &&
166166 (sourceCoordinator.lease == nil || !sourceCoordinator.lease.active || sourceCoordinator.lease.attachment != req.next)
167167 invalid = invalid || !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle)
168- invalid = invalid || req.activateTargetTab && !req.target.activateTargetLocked(req.targetTabIndex)
168+ invalid = invalid || req.activateTargetTab && !req.target.validTargetTabLocked(req.targetTabIndex)
169169 if invalid {
170170 publication.unlockCoordinators()
171171 return nil, errAttachmentTransition
@@ -165,7 +165,7 @@ func (d *Daemon) validateAttachmentTransitionPrelocked(req attachmentTransitionR
165 invalid = invalid || req.sourceToken == nil && source != req.target && req.expectedRole == attachmentActive && sourceCoordinator != nil &&165 invalid = invalid || req.sourceToken == nil && source != req.target && req.expectedRole == attachmentActive && sourceCoordinator != nil &&
166 (sourceCoordinator.lease == nil || !sourceCoordinator.lease.active || sourceCoordinator.lease.attachment != req.next)166 (sourceCoordinator.lease == nil || !sourceCoordinator.lease.active || sourceCoordinator.lease.attachment != req.next)
167 invalid = invalid || !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle)167 invalid = invalid || !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle)
168- invalid = invalid || req.activateTargetTab && !req.target.activateTargetLocked(req.targetTabIndex)168+ invalid = invalid || req.activateTargetTab && !req.target.validTargetTabLocked(req.targetTabIndex)
169 if invalid {169 if invalid {
170 publication.unlockCoordinators()170 publication.unlockCoordinators()
171 return nil, errAttachmentTransition171 return nil, errAttachmentTransition
M internal/usecase/daemon/attention.go +1 -1
@@ -59,7 +59,7 @@ func (d *Daemon) jumpAttentionForRole(sess *session, ac *attachedClient, token a
5959 }
6060 if idx, ok := oldestAttentionTab(sess); ok {
6161 if sess.switchAttachmentTab(ac, idx) {
62- d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(ac), false, ac, token.lease)
62+ d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, token.lease)
6363 d.invalidateRender(sess, ac, true, "attention.go")
6464 }
6565 return nil
@@ -59,7 +59,7 @@ func (d *Daemon) jumpAttentionForRole(sess *session, ac *attachedClient, token a
59 }59 }
60 if idx, ok := oldestAttentionTab(sess); ok {60 if idx, ok := oldestAttentionTab(sess); ok {
61 if sess.switchAttachmentTab(ac, idx) {61 if sess.switchAttachmentTab(ac, idx) {
62- d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(ac), false, ac, token.lease)62+ d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, token.lease)
63 d.invalidateRender(sess, ac, true, "attention.go")63 d.invalidateRender(sess, ac, true, "attention.go")
64 }64 }
65 return nil65 return nil
M internal/usecase/daemon/attention_test.go +21 -21
@@ -23,7 +23,7 @@ func TestNoteAttentionFlagsBackgroundTab(t *testing.T) {
2323 d, sess, _, _, releases := newManualTabSession(t, 2)
2424 defer releases[0]()
2525 defer releases[1]()
26- sess.active = 0
26+ selectTestAttachmentTab(sess, 0)
2727
2828 d.noteAttention(sess, sess.tabs[1])
2929
@@ -37,7 +37,7 @@ func TestNoteAttentionFlagsBackgroundTab(t *testing.T) {
3737 func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {
3838 d, sess, _, _, releases := newManualTabSession(t, 1)
3939 defer releases[0]()
40- sess.active = 0
40+ selectTestAttachmentTab(sess, 0)
4141
4242 d.noteAttention(sess, sess.tabs[0])
4343
@@ -50,7 +50,7 @@ func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {
5050 func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {
5151 d, sess, ac, sends, releases := newManualTabSession(t, 1)
5252 defer releases[0]()
53- sess.active = 0
53+ selectTestAttachmentTab(sess, 0)
5454 d.setAttentionFrame(0)
5555 d.noteAttention(sess, sess.tabs[0])
5656
@@ -86,7 +86,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
8686 t.Run(tt.name, func(t *testing.T) {
8787 pty := newScriptPTY([][]byte{tt.data})
8888 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
89- sess.active = 0
89+ selectTestAttachmentTab(sess, 0)
9090 d.setAttentionFrame(1)
9191
9292 d.sessWg.Add(1)
@@ -119,7 +119,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
119119 func TestNoteAttentionFlagsDetachedActiveTab(t *testing.T) {
120120 d, sess, ac, _, releases := newManualTabSession(t, 1)
121121 defer releases[0]()
122- sess.active = 0
122+ selectTestAttachmentTab(sess, 0)
123123 require.True(t, sess.detachIfCurrent(ac))
124124
125125 d.noteAttention(sess, sess.tabs[0])
@@ -135,7 +135,7 @@ func TestPTYReaderBellMarksBackgroundTab(t *testing.T) {
135135 p2 := newScriptPTY([][]byte{[]byte("\a")})
136136 d, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
137137 defer releasePTY1()
138- sess.active = 0
138+ selectTestAttachmentTab(sess, 0)
139139
140140 d.sessWg.Add(1)
141141 go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane())
@@ -161,7 +161,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
161161 d, sess, _, _, releases := newManualTabSession(t, 2)
162162 defer releases[0]()
163163 defer releases[1]()
164- sess.active = 0
164+ selectTestAttachmentTab(sess, 0)
165165
166166 trW := portsmocks.NewMockTransport(t)
167167 block := make(chan struct{})
@@ -235,7 +235,7 @@ func TestAckAttentionClearsOnlyPaintedVisibleTab(t *testing.T) {
235235 d, sess, ac, sends, releases := newManualTabSession(t, 2)
236236 defer releases[0]()
237237 defer releases[1]()
238- sess.active = 0
238+ selectTestAttachmentTab(sess, 0)
239239 sess.mu.Lock()
240240 sess.tabs[0].attention = true
241241 sess.tabs[0].attentionAt = time.Unix(1, 0)
@@ -266,7 +266,7 @@ func TestPaintPreservesBackgroundAttention(t *testing.T) {
266266 d, sess, ac, sends, releases := newManualTabSession(t, 2)
267267 defer releases[0]()
268268 defer releases[1]()
269- sess.active = 0
269+ selectTestAttachmentTab(sess, 0)
270270 sess.mu.Lock()
271271 sess.tabs[1].attention = true
272272 sess.tabs[1].attentionAt = time.Unix(2, 0)
@@ -286,13 +286,13 @@ func TestSwitchTabClearsAttentionEndToEnd(t *testing.T) {
286286 d, sess, ac, sends, releases := newManualTabSession(t, 2)
287287 defer releases[0]()
288288 defer releases[1]()
289- sess.active = 0
289+ selectTestAttachmentTab(sess, 0)
290290 sess.mu.Lock()
291291 sess.tabs[1].attention = true
292292 sess.tabs[1].attentionAt = time.Unix(2, 0)
293293 sess.mu.Unlock()
294294
295- require.True(t, sess.switchTab(1))
295+ require.True(t, selectTestAttachmentTab(sess, 1))
296296 d.paint(sess, ac, true, nil)
297297 data := mustOutputData(t, sends)
298298 // As above: the deferred all-clients repaint has nothing new to say about
@@ -314,7 +314,7 @@ func TestCloseRingingTabClearsClientBar(t *testing.T) {
314314 d, sess, ac, sends, releases := newManualTabSession(t, 2)
315315 defer releases[0]()
316316 defer releases[1]()
317- sess.active = 0
317+ selectTestAttachmentTab(sess, 0)
318318 sess.mu.Lock()
319319 ringing := sess.tabs[1]
320320 ringing.attention = true
@@ -340,7 +340,7 @@ func TestAnimationRepaintConfinedToBarRows(t *testing.T) {
340340 d, sess, ac, sends, releases := newManualTabSession(t, 2)
341341 defer releases[0]()
342342 defer releases[1]()
343- sess.active = 0
343+ selectTestAttachmentTab(sess, 0)
344344 sess.tabs[0].focusedPane().screen.Write([]byte("MIDSCREENMARKER"))
345345 sess.mu.Lock()
346346 sess.tabs[1].attention = true
@@ -374,7 +374,7 @@ func TestComposeFrameAttentionFrameChangeDamagesOnlyBars(t *testing.T) {
374374 d, sess, _, _, releases := newManualTabSession(t, 2)
375375 defer releases[0]()
376376 defer releases[1]()
377- sess.active = 0
377+ selectTestAttachmentTab(sess, 0)
378378 sess.mu.Lock()
379379 sess.tabs[1].attention = true
380380 sess.tabs[1].attentionAt = time.Unix(1, 0)
@@ -471,7 +471,7 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
471471 defer releases[0]()
472472 defer releases[1]()
473473 defer releases[2]()
474- sess.active = 0
474+ selectTestAttachmentTab(sess, 0)
475475 sess.mu.Lock()
476476 sess.tabs[1].attention = true
477477 sess.tabs[1].attentionAt = time.Unix(20, 0)
@@ -481,8 +481,8 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
481481
482482 d.handleInput(sess, ac, []byte("\x1ba"))
483483
484- require.Equal(t, 2, activeTabIndex(sess))
485- requireFloatingInitialized(t, sess.activeTab())
484+ require.Equal(t, 2, testAttachmentTabIndex(sess))
485+ requireFloatingInitialized(t, testAttachmentTab(sess))
486486 _ = mustOutputData(t, sends)
487487 }
488488
@@ -514,7 +514,7 @@ func TestJumpAttentionCrossesSessionsWhenNoLocalBells(t *testing.T) {
514514 require.Same(t, sess2, ac.currentSession())
515515 require.Contains(t, sess2.snapshotAttachments(), ac)
516516 require.Empty(t, sess1.snapshotAttachments())
517- require.Equal(t, 1, activeTabIndex(sess2))
517+ require.Equal(t, 1, testAttachmentTabIndex(sess2))
518518 _ = mustOutputData(t, sends)
519519 }
520520
@@ -522,12 +522,12 @@ func TestJumpAttentionNoopsWithNoBells(t *testing.T) {
522522 d, sess, ac, sends, releases := newManualTabSession(t, 2)
523523 defer releases[0]()
524524 defer releases[1]()
525- sess.active = 0
525+ selectTestAttachmentTab(sess, 0)
526526
527527 require.NoError(t, d.jumpAttention(sess, ac), "no target to jump to is not a failure")
528528
529529 require.Same(t, sess, ac.currentSession())
530- require.Equal(t, 0, activeTabIndex(sess))
530+ require.Equal(t, 0, testAttachmentTabIndex(sess))
531531 select {
532532 case f := <-sends:
533533 t.Fatalf("unexpected frame on no-op jump: %v", f.Type)
@@ -576,7 +576,7 @@ func TestAttentionAnimatorParksAdvancesAndResets(t *testing.T) {
576576 defer releases[1]()
577577 clk := newManualAttentionClock()
578578 d.clock = clk
579- sess.active = 0
579+ selectTestAttachmentTab(sess, 0)
580580
581581 ctx, cancel := context.WithCancel(context.Background())
582582 done := make(chan struct{})
@@ -23,7 +23,7 @@ func TestNoteAttentionFlagsBackgroundTab(t *testing.T) {
23 d, sess, _, _, releases := newManualTabSession(t, 2)23 d, sess, _, _, releases := newManualTabSession(t, 2)
24 defer releases[0]()24 defer releases[0]()
25 defer releases[1]()25 defer releases[1]()
26- sess.active = 026+ selectTestAttachmentTab(sess, 0)
27 27
28 d.noteAttention(sess, sess.tabs[1])28 d.noteAttention(sess, sess.tabs[1])
29 29
@@ -37,7 +37,7 @@ func TestNoteAttentionFlagsBackgroundTab(t *testing.T) {
37 func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {37 func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {
38 d, sess, _, _, releases := newManualTabSession(t, 1)38 d, sess, _, _, releases := newManualTabSession(t, 1)
39 defer releases[0]()39 defer releases[0]()
40- sess.active = 040+ selectTestAttachmentTab(sess, 0)
41 41
42 d.noteAttention(sess, sess.tabs[0])42 d.noteAttention(sess, sess.tabs[0])
43 43
@@ -50,7 +50,7 @@ func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {
50 func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {50 func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {
51 d, sess, ac, sends, releases := newManualTabSession(t, 1)51 d, sess, ac, sends, releases := newManualTabSession(t, 1)
52 defer releases[0]()52 defer releases[0]()
53- sess.active = 053+ selectTestAttachmentTab(sess, 0)
54 d.setAttentionFrame(0)54 d.setAttentionFrame(0)
55 d.noteAttention(sess, sess.tabs[0])55 d.noteAttention(sess, sess.tabs[0])
56 56
@@ -86,7 +86,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
86 t.Run(tt.name, func(t *testing.T) {86 t.Run(tt.name, func(t *testing.T) {
87 pty := newScriptPTY([][]byte{tt.data})87 pty := newScriptPTY([][]byte{tt.data})
88 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)88 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
89- sess.active = 089+ selectTestAttachmentTab(sess, 0)
90 d.setAttentionFrame(1)90 d.setAttentionFrame(1)
91 91
92 d.sessWg.Add(1)92 d.sessWg.Add(1)
@@ -119,7 +119,7 @@ func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.
119 func TestNoteAttentionFlagsDetachedActiveTab(t *testing.T) {119 func TestNoteAttentionFlagsDetachedActiveTab(t *testing.T) {
120 d, sess, ac, _, releases := newManualTabSession(t, 1)120 d, sess, ac, _, releases := newManualTabSession(t, 1)
121 defer releases[0]()121 defer releases[0]()
122- sess.active = 0122+ selectTestAttachmentTab(sess, 0)
123 require.True(t, sess.detachIfCurrent(ac))123 require.True(t, sess.detachIfCurrent(ac))
124 124
125 d.noteAttention(sess, sess.tabs[0])125 d.noteAttention(sess, sess.tabs[0])
@@ -135,7 +135,7 @@ func TestPTYReaderBellMarksBackgroundTab(t *testing.T) {
135 p2 := newScriptPTY([][]byte{[]byte("\a")})135 p2 := newScriptPTY([][]byte{[]byte("\a")})
136 d, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)136 d, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
137 defer releasePTY1()137 defer releasePTY1()
138- sess.active = 0138+ selectTestAttachmentTab(sess, 0)
139 139
140 d.sessWg.Add(1)140 d.sessWg.Add(1)
141 go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane())141 go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane())
@@ -161,7 +161,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
161 d, sess, _, _, releases := newManualTabSession(t, 2)161 d, sess, _, _, releases := newManualTabSession(t, 2)
162 defer releases[0]()162 defer releases[0]()
163 defer releases[1]()163 defer releases[1]()
164- sess.active = 0164+ selectTestAttachmentTab(sess, 0)
165 165
166 trW := portsmocks.NewMockTransport(t)166 trW := portsmocks.NewMockTransport(t)
167 block := make(chan struct{})167 block := make(chan struct{})
@@ -235,7 +235,7 @@ func TestAckAttentionClearsOnlyPaintedVisibleTab(t *testing.T) {
235 d, sess, ac, sends, releases := newManualTabSession(t, 2)235 d, sess, ac, sends, releases := newManualTabSession(t, 2)
236 defer releases[0]()236 defer releases[0]()
237 defer releases[1]()237 defer releases[1]()
238- sess.active = 0238+ selectTestAttachmentTab(sess, 0)
239 sess.mu.Lock()239 sess.mu.Lock()
240 sess.tabs[0].attention = true240 sess.tabs[0].attention = true
241 sess.tabs[0].attentionAt = time.Unix(1, 0)241 sess.tabs[0].attentionAt = time.Unix(1, 0)
@@ -266,7 +266,7 @@ func TestPaintPreservesBackgroundAttention(t *testing.T) {
266 d, sess, ac, sends, releases := newManualTabSession(t, 2)266 d, sess, ac, sends, releases := newManualTabSession(t, 2)
267 defer releases[0]()267 defer releases[0]()
268 defer releases[1]()268 defer releases[1]()
269- sess.active = 0269+ selectTestAttachmentTab(sess, 0)
270 sess.mu.Lock()270 sess.mu.Lock()
271 sess.tabs[1].attention = true271 sess.tabs[1].attention = true
272 sess.tabs[1].attentionAt = time.Unix(2, 0)272 sess.tabs[1].attentionAt = time.Unix(2, 0)
@@ -286,13 +286,13 @@ func TestSwitchTabClearsAttentionEndToEnd(t *testing.T) {
286 d, sess, ac, sends, releases := newManualTabSession(t, 2)286 d, sess, ac, sends, releases := newManualTabSession(t, 2)
287 defer releases[0]()287 defer releases[0]()
288 defer releases[1]()288 defer releases[1]()
289- sess.active = 0289+ selectTestAttachmentTab(sess, 0)
290 sess.mu.Lock()290 sess.mu.Lock()
291 sess.tabs[1].attention = true291 sess.tabs[1].attention = true
292 sess.tabs[1].attentionAt = time.Unix(2, 0)292 sess.tabs[1].attentionAt = time.Unix(2, 0)
293 sess.mu.Unlock()293 sess.mu.Unlock()
294 294
295- require.True(t, sess.switchTab(1))295+ require.True(t, selectTestAttachmentTab(sess, 1))
296 d.paint(sess, ac, true, nil)296 d.paint(sess, ac, true, nil)
297 data := mustOutputData(t, sends)297 data := mustOutputData(t, sends)
298 // As above: the deferred all-clients repaint has nothing new to say about298 // As above: the deferred all-clients repaint has nothing new to say about
@@ -314,7 +314,7 @@ func TestCloseRingingTabClearsClientBar(t *testing.T) {
314 d, sess, ac, sends, releases := newManualTabSession(t, 2)314 d, sess, ac, sends, releases := newManualTabSession(t, 2)
315 defer releases[0]()315 defer releases[0]()
316 defer releases[1]()316 defer releases[1]()
317- sess.active = 0317+ selectTestAttachmentTab(sess, 0)
318 sess.mu.Lock()318 sess.mu.Lock()
319 ringing := sess.tabs[1]319 ringing := sess.tabs[1]
320 ringing.attention = true320 ringing.attention = true
@@ -340,7 +340,7 @@ func TestAnimationRepaintConfinedToBarRows(t *testing.T) {
340 d, sess, ac, sends, releases := newManualTabSession(t, 2)340 d, sess, ac, sends, releases := newManualTabSession(t, 2)
341 defer releases[0]()341 defer releases[0]()
342 defer releases[1]()342 defer releases[1]()
343- sess.active = 0343+ selectTestAttachmentTab(sess, 0)
344 sess.tabs[0].focusedPane().screen.Write([]byte("MIDSCREENMARKER"))344 sess.tabs[0].focusedPane().screen.Write([]byte("MIDSCREENMARKER"))
345 sess.mu.Lock()345 sess.mu.Lock()
346 sess.tabs[1].attention = true346 sess.tabs[1].attention = true
@@ -374,7 +374,7 @@ func TestComposeFrameAttentionFrameChangeDamagesOnlyBars(t *testing.T) {
374 d, sess, _, _, releases := newManualTabSession(t, 2)374 d, sess, _, _, releases := newManualTabSession(t, 2)
375 defer releases[0]()375 defer releases[0]()
376 defer releases[1]()376 defer releases[1]()
377- sess.active = 0377+ selectTestAttachmentTab(sess, 0)
378 sess.mu.Lock()378 sess.mu.Lock()
379 sess.tabs[1].attention = true379 sess.tabs[1].attention = true
380 sess.tabs[1].attentionAt = time.Unix(1, 0)380 sess.tabs[1].attentionAt = time.Unix(1, 0)
@@ -471,7 +471,7 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
471 defer releases[0]()471 defer releases[0]()
472 defer releases[1]()472 defer releases[1]()
473 defer releases[2]()473 defer releases[2]()
474- sess.active = 0474+ selectTestAttachmentTab(sess, 0)
475 sess.mu.Lock()475 sess.mu.Lock()
476 sess.tabs[1].attention = true476 sess.tabs[1].attention = true
477 sess.tabs[1].attentionAt = time.Unix(20, 0)477 sess.tabs[1].attentionAt = time.Unix(20, 0)
@@ -481,8 +481,8 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
481 481
482 d.handleInput(sess, ac, []byte("\x1ba"))482 d.handleInput(sess, ac, []byte("\x1ba"))
483 483
484- require.Equal(t, 2, activeTabIndex(sess))484+ require.Equal(t, 2, testAttachmentTabIndex(sess))
485- requireFloatingInitialized(t, sess.activeTab())485+ requireFloatingInitialized(t, testAttachmentTab(sess))
486 _ = mustOutputData(t, sends)486 _ = mustOutputData(t, sends)
487 }487 }
488 488
@@ -514,7 +514,7 @@ func TestJumpAttentionCrossesSessionsWhenNoLocalBells(t *testing.T) {
514 require.Same(t, sess2, ac.currentSession())514 require.Same(t, sess2, ac.currentSession())
515 require.Contains(t, sess2.snapshotAttachments(), ac)515 require.Contains(t, sess2.snapshotAttachments(), ac)
516 require.Empty(t, sess1.snapshotAttachments())516 require.Empty(t, sess1.snapshotAttachments())
517- require.Equal(t, 1, activeTabIndex(sess2))517+ require.Equal(t, 1, testAttachmentTabIndex(sess2))
518 _ = mustOutputData(t, sends)518 _ = mustOutputData(t, sends)
519 }519 }
520 520
@@ -522,12 +522,12 @@ func TestJumpAttentionNoopsWithNoBells(t *testing.T) {
522 d, sess, ac, sends, releases := newManualTabSession(t, 2)522 d, sess, ac, sends, releases := newManualTabSession(t, 2)
523 defer releases[0]()523 defer releases[0]()
524 defer releases[1]()524 defer releases[1]()
525- sess.active = 0525+ selectTestAttachmentTab(sess, 0)
526 526
527 require.NoError(t, d.jumpAttention(sess, ac), "no target to jump to is not a failure")527 require.NoError(t, d.jumpAttention(sess, ac), "no target to jump to is not a failure")
528 528
529 require.Same(t, sess, ac.currentSession())529 require.Same(t, sess, ac.currentSession())
530- require.Equal(t, 0, activeTabIndex(sess))530+ require.Equal(t, 0, testAttachmentTabIndex(sess))
531 select {531 select {
532 case f := <-sends:532 case f := <-sends:
533 t.Fatalf("unexpected frame on no-op jump: %v", f.Type)533 t.Fatalf("unexpected frame on no-op jump: %v", f.Type)
@@ -576,7 +576,7 @@ func TestAttentionAnimatorParksAdvancesAndResets(t *testing.T) {
576 defer releases[1]()576 defer releases[1]()
577 clk := newManualAttentionClock()577 clk := newManualAttentionClock()
578 d.clock = clk578 d.clock = clk
579- sess.active = 0579+ selectTestAttachmentTab(sess, 0)
580 580
581 ctx, cancel := context.WithCancel(context.Background())581 ctx, cancel := context.WithCancel(context.Background())
582 done := make(chan struct{})582 done := make(chan struct{})
M internal/usecase/daemon/bar_refresh.go +9 -2
@@ -125,7 +125,7 @@ func (d *Daemon) collectBarScriptContext(sess *session, anchor string) (barScrip
125125 attachments := sess.snapshotAttachmentsLocked()
126126 ctx.PaneCWD = sess.cwd
127127 sess.mu.Unlock()
128- tb := sess.tabForAttachmentOrActive(nil)
128+ tb := sess.firstTab()
129129 if len(attachments) == 0 || tb == nil {
130130 return ctx, env, false
131131 }
@@ -412,7 +412,14 @@ func (d *Daemon) pokeSessionRender(sess *session) {
412412 // A headless session can still supply a picker preview to an attachment
413413 // owned by another session. Its coordinator fans this wake out to those
414414 // preview subscribers even though no primary attachment is bound.
415- for _, ac := range sess.snapshotAttachments() {
415+ attachments := sess.snapshotAttachments()
416+ if len(attachments) == 0 {
417+ if rc := sess.renderCoordinator(); rc != nil {
418+ rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "bar_refresh.go"})
419+ }
420+ return
421+ }
422+ for _, ac := range attachments {
416423 d.invalidateRender(sess, ac, false, "bar_refresh.go")
417424 }
418425 }
@@ -125,7 +125,7 @@ func (d *Daemon) collectBarScriptContext(sess *session, anchor string) (barScrip
125 attachments := sess.snapshotAttachmentsLocked()125 attachments := sess.snapshotAttachmentsLocked()
126 ctx.PaneCWD = sess.cwd126 ctx.PaneCWD = sess.cwd
127 sess.mu.Unlock()127 sess.mu.Unlock()
128- tb := sess.tabForAttachmentOrActive(nil)128+ tb := sess.firstTab()
129 if len(attachments) == 0 || tb == nil {129 if len(attachments) == 0 || tb == nil {
130 return ctx, env, false130 return ctx, env, false
131 }131 }
@@ -412,7 +412,14 @@ func (d *Daemon) pokeSessionRender(sess *session) {
412 // A headless session can still supply a picker preview to an attachment412 // A headless session can still supply a picker preview to an attachment
413 // owned by another session. Its coordinator fans this wake out to those413 // owned by another session. Its coordinator fans this wake out to those
414 // preview subscribers even though no primary attachment is bound.414 // preview subscribers even though no primary attachment is bound.
415- for _, ac := range sess.snapshotAttachments() {415+ attachments := sess.snapshotAttachments()
416+ if len(attachments) == 0 {
417+ if rc := sess.renderCoordinator(); rc != nil {
418+ rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "bar_refresh.go"})
419+ }
420+ return
421+ }
422+ for _, ac := range attachments {
416 d.invalidateRender(sess, ac, false, "bar_refresh.go")423 d.invalidateRender(sess, ac, false, "bar_refresh.go")
417 }424 }
418 }425 }
M internal/usecase/daemon/bar_refresh_test.go +7 -8
@@ -85,17 +85,16 @@ func TestBarScriptRefreshIntervalClampingTickAndLastGoodOnFailure(t *testing.T)
8585 require.Equal(t, "bot2", state.bottomRight)
8686 }
8787
88-func TestBarScriptContextUsesActivePaneSessionAndClientCols(t *testing.T) {
88+func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
8989 r := &fakeBarRunner{outs: []string{"top", "bottom"}}
9090 d := newBarRefreshTestDaemon(r, time.Second)
9191 sess := newBarRefreshTestSession()
9292 sess.name = "work"
9393 sess.cwd = "/repo"
9494 sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 132, Rows: 40}})
95- sess.active = 1
96- sess.tabs[1].size = domain.Size{Cols: 132, Rows: 38}
97- active := sess.tabs[1].focusedPane()
98- active.stableID = "pane-active"
95+ sess.tabs[0].size = domain.Size{Cols: 132, Rows: 38}
96+ active := sess.tabs[0].focusedPane()
97+ active.stableID = "pane-default"
9998 active.pty = newScriptPTY(nil)
10099 d.procCwd = func(pid int) (string, error) {
101100 require.Equal(t, 4242, pid)
@@ -107,8 +106,8 @@ func TestBarScriptContextUsesActivePaneSessionAndClientCols(t *testing.T) {
107106 require.Len(t, r.calls, 2)
108107 for _, call := range r.calls {
109108 require.Equal(t, "work", call.Session)
110- require.Equal(t, "tab-active", call.Tab)
111- require.Equal(t, "pane-active", call.Pane)
109+ require.Equal(t, "tab-inactive", call.Tab)
110+ require.Equal(t, "pane-default", call.Pane)
112111 require.Equal(t, "/pane-repo", call.PaneCWD)
113112 require.Equal(t, 132, call.Cols)
114113 }
@@ -587,7 +586,7 @@ func newBarRefreshTestSession() *session {
587586 inactive := newTabWithStableID("tab-inactive", "pane-inactive", nil, domain.Size{Cols: 80, Rows: 22})
588587 active := newTabWithStableID("tab-active", "pane-active", nil, domain.Size{Cols: 80, Rows: 22})
589588 active.tree.Focus = layout.PaneID("pane-1")
590- return &session{sessionCore: sessionCore{id: "s", name: "s"}, cwd: "/tmp", tabs: []*tab{inactive, active}, active: 0, ctx: context.Background()}
589+ return &session{sessionCore: sessionCore{id: "s", name: "s"}, cwd: "/tmp", tabs: []*tab{inactive, active}, ctx: context.Background()}
591590 }
592591
593592 func waitBarRefreshIdle(t *testing.T, d *Daemon) {
@@ -85,17 +85,16 @@ func TestBarScriptRefreshIntervalClampingTickAndLastGoodOnFailure(t *testing.T)
85 require.Equal(t, "bot2", state.bottomRight)85 require.Equal(t, "bot2", state.bottomRight)
86 }86 }
87 87
88-func TestBarScriptContextUsesActivePaneSessionAndClientCols(t *testing.T) {88+func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
89 r := &fakeBarRunner{outs: []string{"top", "bottom"}}89 r := &fakeBarRunner{outs: []string{"top", "bottom"}}
90 d := newBarRefreshTestDaemon(r, time.Second)90 d := newBarRefreshTestDaemon(r, time.Second)
91 sess := newBarRefreshTestSession()91 sess := newBarRefreshTestSession()
92 sess.name = "work"92 sess.name = "work"
93 sess.cwd = "/repo"93 sess.cwd = "/repo"
94 sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 132, Rows: 40}})94 sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 132, Rows: 40}})
95- sess.active = 195+ sess.tabs[0].size = domain.Size{Cols: 132, Rows: 38}
96- sess.tabs[1].size = domain.Size{Cols: 132, Rows: 38}96+ active := sess.tabs[0].focusedPane()
97- active := sess.tabs[1].focusedPane()97+ active.stableID = "pane-default"
98- active.stableID = "pane-active"
99 active.pty = newScriptPTY(nil)98 active.pty = newScriptPTY(nil)
100 d.procCwd = func(pid int) (string, error) {99 d.procCwd = func(pid int) (string, error) {
101 require.Equal(t, 4242, pid)100 require.Equal(t, 4242, pid)
@@ -107,8 +106,8 @@ func TestBarScriptContextUsesActivePaneSessionAndClientCols(t *testing.T) {
107 require.Len(t, r.calls, 2)106 require.Len(t, r.calls, 2)
108 for _, call := range r.calls {107 for _, call := range r.calls {
109 require.Equal(t, "work", call.Session)108 require.Equal(t, "work", call.Session)
110- require.Equal(t, "tab-active", call.Tab)109+ require.Equal(t, "tab-inactive", call.Tab)
111- require.Equal(t, "pane-active", call.Pane)110+ require.Equal(t, "pane-default", call.Pane)
112 require.Equal(t, "/pane-repo", call.PaneCWD)111 require.Equal(t, "/pane-repo", call.PaneCWD)
113 require.Equal(t, 132, call.Cols)112 require.Equal(t, 132, call.Cols)
114 }113 }
@@ -587,7 +586,7 @@ func newBarRefreshTestSession() *session {
587 inactive := newTabWithStableID("tab-inactive", "pane-inactive", nil, domain.Size{Cols: 80, Rows: 22})586 inactive := newTabWithStableID("tab-inactive", "pane-inactive", nil, domain.Size{Cols: 80, Rows: 22})
588 active := newTabWithStableID("tab-active", "pane-active", nil, domain.Size{Cols: 80, Rows: 22})587 active := newTabWithStableID("tab-active", "pane-active", nil, domain.Size{Cols: 80, Rows: 22})
589 active.tree.Focus = layout.PaneID("pane-1")588 active.tree.Focus = layout.PaneID("pane-1")
590- return &session{sessionCore: sessionCore{id: "s", name: "s"}, cwd: "/tmp", tabs: []*tab{inactive, active}, active: 0, ctx: context.Background()}589+ return &session{sessionCore: sessionCore{id: "s", name: "s"}, cwd: "/tmp", tabs: []*tab{inactive, active}, ctx: context.Background()}
591 }590 }
592 591
593 func waitBarRefreshIdle(t *testing.T, d *Daemon) {592 func waitBarRefreshIdle(t *testing.T, d *Daemon) {
M internal/usecase/daemon/client.go +1 -1
@@ -718,7 +718,7 @@ func (d *Daemon) firstPaintWithLease(sess *session, ac *attachedClient, clientSi
718718 // Drained before the early return below so a session without an active tab
719719 // cannot swallow the queue.
720720 d.drainPendingForFirstPaint(sess, ac)
721- tb := sess.tabForAttachmentOrActive(ac)
721+ tb := sess.tabForAttachment(ac)
722722 if tb == nil {
723723 return
724724 }
@@ -718,7 +718,7 @@ func (d *Daemon) firstPaintWithLease(sess *session, ac *attachedClient, clientSi
718 // Drained before the early return below so a session without an active tab718 // Drained before the early return below so a session without an active tab
719 // cannot swallow the queue.719 // cannot swallow the queue.
720 d.drainPendingForFirstPaint(sess, ac)720 d.drainPendingForFirstPaint(sess, ac)
721- tb := sess.tabForAttachmentOrActive(ac)721+ tb := sess.tabForAttachment(ac)
722 if tb == nil {722 if tb == nil {
723 return723 return
724 }724 }
M internal/usecase/daemon/clipboard.go +6 -1
@@ -147,7 +147,12 @@ func (d *Daemon) injectClipboardPathToTarget(sess *session, path string, token *
147147 if token != nil {
148148 ac = token.ac
149149 }
150- tb := sess.tabForAttachmentOrActive(ac)
150+ var tb *tab
151+ if ac != nil {
152+ tb = sess.tabForAttachment(ac)
153+ } else {
154+ tb = sess.firstTab()
155+ }
151156 if tb == nil {
152157 return
153158 }
@@ -147,7 +147,12 @@ func (d *Daemon) injectClipboardPathToTarget(sess *session, path string, token *
147 if token != nil {147 if token != nil {
148 ac = token.ac148 ac = token.ac
149 }149 }
150- tb := sess.tabForAttachmentOrActive(ac)150+ var tb *tab
151+ if ac != nil {
152+ tb = sess.tabForAttachment(ac)
153+ } else {
154+ tb = sess.firstTab()
155+ }
151 if tb == nil {156 if tb == nil {
152 return157 return
153 }158 }
M internal/usecase/daemon/clipboard_test.go +5 -5
@@ -31,7 +31,7 @@ func TestInjectClipboardPathTargetsVisibleFloatingPane(t *testing.T) {
3131 d, sess, _, _ := newManualSessionWithPTYs(t, normal)
3232 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})
3333 floating.screen.Write([]byte("\x1b[?2004h"))
34- installTestFloating(sess.activeTab(), floating, true)
34+ installTestFloating(testAttachmentTab(sess), floating, true)
3535
3636 d.injectClipboardPath(sess, "/tmp/image.png")
3737
@@ -211,7 +211,7 @@ func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClie
211211
212212 func clipboardOwnerLease(t *testing.T, sess *session) paneEffectLease {
213213 t.Helper()
214- tb := sess.activeTab()
214+ tb := testAttachmentTab(sess)
215215 tb.mu.Lock()
216216 p := tb.terminalTargetLocked()
217217 tb.mu.Unlock()
@@ -354,7 +354,7 @@ func TestQueuedClipboardAfterPaneMoveDoesNotSendToFormerOwner(t *testing.T) {
354354 sess.mu.Unlock()
355355 d.forwardClipboardAsync(clipboardOwnerLease(t, sess), base64.StdEncoding.EncodeToString([]byte("queued")))
356356
357- sourceTab := sess.activeTab()
357+ sourceTab := testAttachmentTab(sess)
358358 p := sourceTab.focusedPane()
359359 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
360360 publishPaneOwner(p, destination, &tab{}, 0)
@@ -422,7 +422,7 @@ func TestQueuedClipboardRevalidatesOwnerAfterWaitingForClientSendLock(t *testing
422422 return ac.roleEffects.inFlightCount() == 1
423423 }, time.Second, time.Millisecond, "clipboard send was not admitted before waiting on sendMu")
424424
425- sourceTab := sess.activeTab()
425+ sourceTab := testAttachmentTab(sess)
426426 p := sourceTab.focusedPane()
427427 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
428428 publishPaneOwner(p, destination, &tab{}, 0)
@@ -453,7 +453,7 @@ func TestClipboardSendErrorAfterPaneMoveDoesNotDetachFormerOwner(t *testing.T) {
453453 }()
454454 awaitTestCompletion(t, oldTransport.started, "clipboard send did not start")
455455
456- sourceTab := sess.activeTab()
456+ sourceTab := testAttachmentTab(sess)
457457 p := sourceTab.focusedPane()
458458 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
459459 publishPaneOwner(p, destination, &tab{}, 0)
@@ -31,7 +31,7 @@ func TestInjectClipboardPathTargetsVisibleFloatingPane(t *testing.T) {
31 d, sess, _, _ := newManualSessionWithPTYs(t, normal)31 d, sess, _, _ := newManualSessionWithPTYs(t, normal)
32 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})32 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})
33 floating.screen.Write([]byte("\x1b[?2004h"))33 floating.screen.Write([]byte("\x1b[?2004h"))
34- installTestFloating(sess.activeTab(), floating, true)34+ installTestFloating(testAttachmentTab(sess), floating, true)
35 35
36 d.injectClipboardPath(sess, "/tmp/image.png")36 d.injectClipboardPath(sess, "/tmp/image.png")
37 37
@@ -211,7 +211,7 @@ func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClie
211 211
212 func clipboardOwnerLease(t *testing.T, sess *session) paneEffectLease {212 func clipboardOwnerLease(t *testing.T, sess *session) paneEffectLease {
213 t.Helper()213 t.Helper()
214- tb := sess.activeTab()214+ tb := testAttachmentTab(sess)
215 tb.mu.Lock()215 tb.mu.Lock()
216 p := tb.terminalTargetLocked()216 p := tb.terminalTargetLocked()
217 tb.mu.Unlock()217 tb.mu.Unlock()
@@ -354,7 +354,7 @@ func TestQueuedClipboardAfterPaneMoveDoesNotSendToFormerOwner(t *testing.T) {
354 sess.mu.Unlock()354 sess.mu.Unlock()
355 d.forwardClipboardAsync(clipboardOwnerLease(t, sess), base64.StdEncoding.EncodeToString([]byte("queued")))355 d.forwardClipboardAsync(clipboardOwnerLease(t, sess), base64.StdEncoding.EncodeToString([]byte("queued")))
356 356
357- sourceTab := sess.activeTab()357+ sourceTab := testAttachmentTab(sess)
358 p := sourceTab.focusedPane()358 p := sourceTab.focusedPane()
359 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}359 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
360 publishPaneOwner(p, destination, &tab{}, 0)360 publishPaneOwner(p, destination, &tab{}, 0)
@@ -422,7 +422,7 @@ func TestQueuedClipboardRevalidatesOwnerAfterWaitingForClientSendLock(t *testing
422 return ac.roleEffects.inFlightCount() == 1422 return ac.roleEffects.inFlightCount() == 1
423 }, time.Second, time.Millisecond, "clipboard send was not admitted before waiting on sendMu")423 }, time.Second, time.Millisecond, "clipboard send was not admitted before waiting on sendMu")
424 424
425- sourceTab := sess.activeTab()425+ sourceTab := testAttachmentTab(sess)
426 p := sourceTab.focusedPane()426 p := sourceTab.focusedPane()
427 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}427 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
428 publishPaneOwner(p, destination, &tab{}, 0)428 publishPaneOwner(p, destination, &tab{}, 0)
@@ -453,7 +453,7 @@ func TestClipboardSendErrorAfterPaneMoveDoesNotDetachFormerOwner(t *testing.T) {
453 }()453 }()
454 awaitTestCompletion(t, oldTransport.started, "clipboard send did not start")454 awaitTestCompletion(t, oldTransport.started, "clipboard send did not start")
455 455
456- sourceTab := sess.activeTab()456+ sourceTab := testAttachmentTab(sess)
457 p := sourceTab.focusedPane()457 p := sourceTab.focusedPane()
458 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}458 destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}}
459 publishPaneOwner(p, destination, &tab{}, 0)459 publishPaneOwner(p, destination, &tab{}, 0)
M internal/usecase/daemon/control.go +11 -4
@@ -276,7 +276,10 @@ func resolveDaemonActionTargetForAttachment(sess *session, ac *attachedClient) d
276276 if sess == nil {
277277 return daemonActionTarget{}
278278 }
279- tb := sess.tabForAttachmentOrActive(ac)
279+ tb := sess.tabForAttachment(ac)
280+ if ac == nil {
281+ tb = sess.firstTab()
282+ }
280283 if tb == nil {
281284 return daemonActionTarget{session: sess}
282285 }
@@ -364,8 +367,8 @@ func (d *Daemon) hasDaemonActionPaneTarget(target daemonActionTarget) bool {
364367 }
365368
366369 func (a daemonActions) switchRelative(sess *session, ac *attachedClient, delta int) error {
367- if sess.switchAttachmentRelative(ac, delta) {
368- a.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(ac), false, ac, nil)
370+ if sess.switchAttachmentRelativeForDispatch(ac, delta) {
371+ a.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, nil)
369372 }
370373 return nil
371374 }
@@ -689,7 +692,11 @@ func (e controlExec) ListPanes(asJSON bool) (string, error) {
689692 }
690693 tb := e.tab
691694 if tb == nil {
692- tb = e.sess.tabForAttachmentOrActive(e.target.attachment)
695+ if e.target.attachment == nil {
696+ tb = e.sess.firstTab()
697+ } else {
698+ tb = e.sess.tabForAttachment(e.target.attachment)
699+ }
693700 }
694701 if tb == nil {
695702 return "", errors.New("target session has no active tab")
@@ -276,7 +276,10 @@ func resolveDaemonActionTargetForAttachment(sess *session, ac *attachedClient) d
276 if sess == nil {276 if sess == nil {
277 return daemonActionTarget{}277 return daemonActionTarget{}
278 }278 }
279- tb := sess.tabForAttachmentOrActive(ac)279+ tb := sess.tabForAttachment(ac)
280+ if ac == nil {
281+ tb = sess.firstTab()
282+ }
280 if tb == nil {283 if tb == nil {
281 return daemonActionTarget{session: sess}284 return daemonActionTarget{session: sess}
282 }285 }
@@ -364,8 +367,8 @@ func (d *Daemon) hasDaemonActionPaneTarget(target daemonActionTarget) bool {
364 }367 }
365 368
366 func (a daemonActions) switchRelative(sess *session, ac *attachedClient, delta int) error {369 func (a daemonActions) switchRelative(sess *session, ac *attachedClient, delta int) error {
367- if sess.switchAttachmentRelative(ac, delta) {370+ if sess.switchAttachmentRelativeForDispatch(ac, delta) {
368- a.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(ac), false, ac, nil)371+ a.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, nil)
369 }372 }
370 return nil373 return nil
371 }374 }
@@ -689,7 +692,11 @@ func (e controlExec) ListPanes(asJSON bool) (string, error) {
689 }692 }
690 tb := e.tab693 tb := e.tab
691 if tb == nil {694 if tb == nil {
692- tb = e.sess.tabForAttachmentOrActive(e.target.attachment)695+ if e.target.attachment == nil {
696+ tb = e.sess.firstTab()
697+ } else {
698+ tb = e.sess.tabForAttachment(e.target.attachment)
699+ }
693 }700 }
694 if tb == nil {701 if tb == nil {
695 return "", errors.New("target session has no active tab")702 return "", errors.New("target session has no active tab")
M internal/usecase/daemon/control_test.go +26 -28
@@ -37,7 +37,7 @@ func TestPaletteAndControlShareExplicitDaemonActionTarget(t *testing.T) {
3737 t.Run(tt.name, func(t *testing.T) {
3838 d := newTestDaemon(t, nil, stubClock{})
3939 sess := addControlSession(d, "work", "t_work", "p_work")
40- tb := sess.activeTab()
40+ tb := testAttachmentTab(sess)
4141 tb.mu.Lock()
4242 pane := tb.focusedPane()
4343 tb.mu.Unlock()
@@ -78,9 +78,7 @@ func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) {
7878
7979 require.True(t, result.OK, result.Text)
8080 require.Empty(t, result.Output)
81- h.session.mu.Lock()
82- require.Zero(t, h.session.active, "--self must not change the active tab")
83- h.session.mu.Unlock()
81+ require.Zero(t, testAttachmentTabIndex(h.session), "--self must not change the attachment tab")
8482 h.tab.mu.Lock()
8583 require.Equal(t, layout.PaneID("pane-3"), h.tab.tree.Focus, "a moved explicit target becomes focused")
8684 require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration)
@@ -240,7 +238,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
240238 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
241239 sess.mu.Lock()
242240 sess.tabs = append(sess.tabs, second)
243- sess.active = 0
241+ selectTestAttachmentTabLocked(sess, 0)
244242 sess.mu.Unlock()
245243
246244 result := sendCommand(t, d, ports.CommandRequest{
@@ -256,7 +254,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
256254 require.Empty(t, second.name)
257255 second.mu.Unlock()
258256 sess.mu.Lock()
259- require.Zero(t, sess.active)
257+ require.Zero(t, testAttachmentTabIndexLocked(sess))
260258 sess.mu.Unlock()
261259 }
262260
@@ -272,7 +270,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
272270 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
273271 sess.mu.Lock()
274272 sess.tabs = append(sess.tabs, second)
275- sess.active = 0
273+ selectTestAttachmentTabLocked(sess, 0)
276274 sess.mu.Unlock()
277275
278276 result := sendCommand(t, d, ports.CommandRequest{
@@ -282,7 +280,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
282280
283281 require.True(t, result.OK, result.Text)
284282 sess.mu.Lock()
285- activeIndex := sess.active
283+ activeIndex := testAttachmentTabIndexLocked(sess)
286284 sess.mu.Unlock()
287285 require.Zero(t, activeIndex)
288286 active.mu.Lock()
@@ -309,7 +307,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
309307 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)
310308 sess.mu.Lock()
311309 sess.tabs = append(sess.tabs, invoking)
312- sess.active = 0
310+ selectTestAttachmentTabLocked(sess, 0)
313311 sess.mu.Unlock()
314312
315313 rename := sendCommand(t, d, ports.CommandRequest{
@@ -383,7 +381,7 @@ func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
383381 sess := addControlSession(d, "work", "t_work", "p_work")
384382 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
385383 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)
386- tb := sess.activeTab()
384+ tb := testAttachmentTab(sess)
387385 tb.mu.Lock()
388386 generation := tb.layoutGeneration
389387 tb.mu.Unlock()
@@ -422,7 +420,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
422420 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)
423421 sess.mu.Lock()
424422 sess.tabs = append(sess.tabs, locator)
425- sess.active = 0
423+ selectTestAttachmentTabLocked(sess, 0)
426424 sess.mu.Unlock()
427425 active.mu.Lock()
428426 before := active.layoutGeneration
@@ -439,7 +437,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
439437 require.Zero(t, locator.layoutGeneration)
440438 locator.mu.Unlock()
441439 sess.mu.Lock()
442- require.Zero(t, sess.active)
440+ require.Zero(t, testAttachmentTabIndexLocked(sess))
443441 sess.mu.Unlock()
444442 }
445443
@@ -449,7 +447,7 @@ func TestResizeControlTooSmallUsesStableFailure(t *testing.T) {
449447 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
450448 sess := addControlSession(d, "work", "t_work", "p_work")
451449 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
452- tb := sess.activeTab()
450+ tb := testAttachmentTab(sess)
453451 tb.mu.Lock()
454452 tb.size.Cols = 41
455453 tb.bumpLayoutGenerationLocked()
@@ -468,7 +466,7 @@ func TestHandleCommandSelfListPanesUsesInvokingTab(t *testing.T) {
468466 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)
469467 sess.mu.Lock()
470468 sess.tabs = append(sess.tabs, invoking)
471- sess.active = 0
469+ selectTestAttachmentTabLocked(sess, 0)
472470 sess.mu.Unlock()
473471
474472 result := sendCommand(t, d, ports.CommandRequest{
@@ -539,7 +537,7 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
539537 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
540538 sess.mu.Lock()
541539 sess.tabs = append(sess.tabs, second)
542- sess.active = 1
540+ selectTestAttachmentTabLocked(sess, 1)
543541 sess.mu.Unlock()
544542
545543 result := sendCommand(t, d, ports.CommandRequest{Slug: "close-tab", TargetSession: "work"})
@@ -560,7 +558,7 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
560558 verify func(*testing.T, *Daemon, *session)
561559 }{
562560 {name: "split focused pane", slug: "split-right", verify: func(t *testing.T, _ *Daemon, sess *session) {
563- tb := sess.activeTab()
561+ tb := testAttachmentTab(sess)
564562 tb.mu.Lock()
565563 defer tb.mu.Unlock()
566564 require.Len(t, tb.panes, 2)
@@ -569,10 +567,10 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
569567 sess.mu.Lock()
570568 defer sess.mu.Unlock()
571569 require.Len(t, sess.tabs, 2)
572- require.Equal(t, 1, sess.active)
570+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess))
573571 }},
574572 {name: "rename tab", slug: "rename-tab", args: []string{"logs"}, verify: func(t *testing.T, _ *Daemon, sess *session) {
575- tb := sess.activeTab()
573+ tb := testAttachmentTab(sess)
576574 tb.mu.Lock()
577575 defer tb.mu.Unlock()
578576 require.Equal(t, "logs", tb.name)
@@ -762,7 +760,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
762760 target.ctx, target.cancel = context.WithCancel(d.serveCtx)
763761 sess.mu.Lock()
764762 sess.tabs = append(sess.tabs, target)
765- sess.active = 0
763+ selectTestAttachmentTabLocked(sess, 0)
766764 sess.mu.Unlock()
767765 original := target.tree.Focus
768766
@@ -808,7 +806,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
808806 require.True(t, awaitCommandResult(t, secondSends).OK)
809807
810808 sess.mu.Lock()
811- require.Zero(t, sess.active, "self commands must not select the invoking tab")
809+ require.Zero(t, testAttachmentTabIndexLocked(sess), "self commands must not select the invoking tab")
812810 sess.mu.Unlock()
813811 target.mu.Lock()
814812 defer target.mu.Unlock()
@@ -824,7 +822,7 @@ func TestHandleCommandMovePaneUsesActiveFocusedSourceWithSessionFlag(t *testing.
824822 publishTiledPaneOwners(source, inactive)
825823 source.mu.Lock()
826824 source.tabs = append(source.tabs, inactive)
827- source.active = 0
825+ selectTestAttachmentTabLocked(source, 0)
828826 source.mu.Unlock()
829827 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
830828
@@ -859,7 +857,7 @@ func TestHandleCommandMovePaneSelfUsesStableSourceIDs(t *testing.T) {
859857 publishTiledPaneOwners(source, inactive)
860858 source.mu.Lock()
861859 source.tabs = append(source.tabs, inactive)
862- source.active = 0
860+ selectTestAttachmentTabLocked(source, 0)
863861 source.mu.Unlock()
864862 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
865863
@@ -896,7 +894,7 @@ func TestHandleCommandMoveTabUsesActiveTabWithoutSelf(t *testing.T) {
896894 publishTiledPaneOwners(source, second)
897895 source.mu.Lock()
898896 source.tabs = append(source.tabs, second)
899- source.active = 1
897+ selectTestAttachmentTabLocked(source, 1)
900898 source.mu.Unlock()
901899 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
902900
@@ -923,7 +921,7 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
923921 publishTiledPaneOwners(source, second)
924922 source.mu.Lock()
925923 source.tabs = append(source.tabs, second)
926- source.active = 0
924+ selectTestAttachmentTabLocked(source, 0)
927925 source.mu.Unlock()
928926 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
929927
@@ -1093,7 +1091,7 @@ func TestHandleCommandMovePaneStableIDsLocateSessionWithoutSelfRedirect(t *testi
10931091 publishTiledPaneOwners(source, inactive)
10941092 source.mu.Lock()
10951093 source.tabs = append(source.tabs, inactive)
1096- source.active = 1
1094+ selectTestAttachmentTabLocked(source, 1)
10971095 source.mu.Unlock()
10981096 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
10991097
@@ -1126,7 +1124,7 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
11261124 left.mu.Lock()
11271125 left.name = "left"
11281126 left.tabs[0].stableID = "left-moved"
1129- left.active = 0
1127+ selectTestAttachmentTabLocked(left, 0)
11301128 left.mu.Unlock()
11311129
11321130 right := addMoveTabTestSession(d, "right", "right-stays")
@@ -1135,7 +1133,7 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
11351133 publishTiledPaneOwners(right, rightMoved)
11361134 right.mu.Lock()
11371135 right.tabs = append(right.tabs, rightMoved)
1138- right.active = 1
1136+ selectTestAttachmentTabLocked(right, 1)
11391137 right.mu.Unlock()
11401138
11411139 leftFrame := commandFrame(t, ports.CommandRequest{
@@ -1179,7 +1177,7 @@ func addNamedMoveDestination(d *Daemon, name, tabID, paneID string) *session {
11791177 tb := newTabWithStableID(tabID, paneID, newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
11801178 tb.ctx, tb.cancel = context.WithCancel(ctx)
11811179 sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, incarnation: domain.IncarnationID{5},
1182- ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}, active: 0,
1180+ ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb},
11831181 }
11841182 publishTiledPaneOwners(sess, tb)
11851183 d.mu.Lock()
@@ -37,7 +37,7 @@ func TestPaletteAndControlShareExplicitDaemonActionTarget(t *testing.T) {
37 t.Run(tt.name, func(t *testing.T) {37 t.Run(tt.name, func(t *testing.T) {
38 d := newTestDaemon(t, nil, stubClock{})38 d := newTestDaemon(t, nil, stubClock{})
39 sess := addControlSession(d, "work", "t_work", "p_work")39 sess := addControlSession(d, "work", "t_work", "p_work")
40- tb := sess.activeTab()40+ tb := testAttachmentTab(sess)
41 tb.mu.Lock()41 tb.mu.Lock()
42 pane := tb.focusedPane()42 pane := tb.focusedPane()
43 tb.mu.Unlock()43 tb.mu.Unlock()
@@ -78,9 +78,7 @@ func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) {
78 78
79 require.True(t, result.OK, result.Text)79 require.True(t, result.OK, result.Text)
80 require.Empty(t, result.Output)80 require.Empty(t, result.Output)
81- h.session.mu.Lock()81+ require.Zero(t, testAttachmentTabIndex(h.session), "--self must not change the attachment tab")
82- require.Zero(t, h.session.active, "--self must not change the active tab")
83- h.session.mu.Unlock()
84 h.tab.mu.Lock()82 h.tab.mu.Lock()
85 require.Equal(t, layout.PaneID("pane-3"), h.tab.tree.Focus, "a moved explicit target becomes focused")83 require.Equal(t, layout.PaneID("pane-3"), h.tab.tree.Focus, "a moved explicit target becomes focused")
86 require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration)84 require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration)
@@ -240,7 +238,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
240 second.ctx, second.cancel = context.WithCancel(d.serveCtx)238 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
241 sess.mu.Lock()239 sess.mu.Lock()
242 sess.tabs = append(sess.tabs, second)240 sess.tabs = append(sess.tabs, second)
243- sess.active = 0241+ selectTestAttachmentTabLocked(sess, 0)
244 sess.mu.Unlock()242 sess.mu.Unlock()
245 243
246 result := sendCommand(t, d, ports.CommandRequest{244 result := sendCommand(t, d, ports.CommandRequest{
@@ -256,7 +254,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
256 require.Empty(t, second.name)254 require.Empty(t, second.name)
257 second.mu.Unlock()255 second.mu.Unlock()
258 sess.mu.Lock()256 sess.mu.Lock()
259- require.Zero(t, sess.active)257+ require.Zero(t, testAttachmentTabIndexLocked(sess))
260 sess.mu.Unlock()258 sess.mu.Unlock()
261 }259 }
262 260
@@ -272,7 +270,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
272 second.ctx, second.cancel = context.WithCancel(d.serveCtx)270 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
273 sess.mu.Lock()271 sess.mu.Lock()
274 sess.tabs = append(sess.tabs, second)272 sess.tabs = append(sess.tabs, second)
275- sess.active = 0273+ selectTestAttachmentTabLocked(sess, 0)
276 sess.mu.Unlock()274 sess.mu.Unlock()
277 275
278 result := sendCommand(t, d, ports.CommandRequest{276 result := sendCommand(t, d, ports.CommandRequest{
@@ -282,7 +280,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
282 280
283 require.True(t, result.OK, result.Text)281 require.True(t, result.OK, result.Text)
284 sess.mu.Lock()282 sess.mu.Lock()
285- activeIndex := sess.active283+ activeIndex := testAttachmentTabIndexLocked(sess)
286 sess.mu.Unlock()284 sess.mu.Unlock()
287 require.Zero(t, activeIndex)285 require.Zero(t, activeIndex)
288 active.mu.Lock()286 active.mu.Lock()
@@ -309,7 +307,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
309 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)307 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)
310 sess.mu.Lock()308 sess.mu.Lock()
311 sess.tabs = append(sess.tabs, invoking)309 sess.tabs = append(sess.tabs, invoking)
312- sess.active = 0310+ selectTestAttachmentTabLocked(sess, 0)
313 sess.mu.Unlock()311 sess.mu.Unlock()
314 312
315 rename := sendCommand(t, d, ports.CommandRequest{313 rename := sendCommand(t, d, ports.CommandRequest{
@@ -383,7 +381,7 @@ func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
383 sess := addControlSession(d, "work", "t_work", "p_work")381 sess := addControlSession(d, "work", "t_work", "p_work")
384 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)382 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
385 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)383 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)
386- tb := sess.activeTab()384+ tb := testAttachmentTab(sess)
387 tb.mu.Lock()385 tb.mu.Lock()
388 generation := tb.layoutGeneration386 generation := tb.layoutGeneration
389 tb.mu.Unlock()387 tb.mu.Unlock()
@@ -422,7 +420,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
422 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)420 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)
423 sess.mu.Lock()421 sess.mu.Lock()
424 sess.tabs = append(sess.tabs, locator)422 sess.tabs = append(sess.tabs, locator)
425- sess.active = 0423+ selectTestAttachmentTabLocked(sess, 0)
426 sess.mu.Unlock()424 sess.mu.Unlock()
427 active.mu.Lock()425 active.mu.Lock()
428 before := active.layoutGeneration426 before := active.layoutGeneration
@@ -439,7 +437,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
439 require.Zero(t, locator.layoutGeneration)437 require.Zero(t, locator.layoutGeneration)
440 locator.mu.Unlock()438 locator.mu.Unlock()
441 sess.mu.Lock()439 sess.mu.Lock()
442- require.Zero(t, sess.active)440+ require.Zero(t, testAttachmentTabIndexLocked(sess))
443 sess.mu.Unlock()441 sess.mu.Unlock()
444 }442 }
445 443
@@ -449,7 +447,7 @@ func TestResizeControlTooSmallUsesStableFailure(t *testing.T) {
449 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })447 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
450 sess := addControlSession(d, "work", "t_work", "p_work")448 sess := addControlSession(d, "work", "t_work", "p_work")
451 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)449 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
452- tb := sess.activeTab()450+ tb := testAttachmentTab(sess)
453 tb.mu.Lock()451 tb.mu.Lock()
454 tb.size.Cols = 41452 tb.size.Cols = 41
455 tb.bumpLayoutGenerationLocked()453 tb.bumpLayoutGenerationLocked()
@@ -468,7 +466,7 @@ func TestHandleCommandSelfListPanesUsesInvokingTab(t *testing.T) {
468 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)466 invoking.ctx, invoking.cancel = context.WithCancel(d.serveCtx)
469 sess.mu.Lock()467 sess.mu.Lock()
470 sess.tabs = append(sess.tabs, invoking)468 sess.tabs = append(sess.tabs, invoking)
471- sess.active = 0469+ selectTestAttachmentTabLocked(sess, 0)
472 sess.mu.Unlock()470 sess.mu.Unlock()
473 471
474 result := sendCommand(t, d, ports.CommandRequest{472 result := sendCommand(t, d, ports.CommandRequest{
@@ -539,7 +537,7 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
539 second.ctx, second.cancel = context.WithCancel(d.serveCtx)537 second.ctx, second.cancel = context.WithCancel(d.serveCtx)
540 sess.mu.Lock()538 sess.mu.Lock()
541 sess.tabs = append(sess.tabs, second)539 sess.tabs = append(sess.tabs, second)
542- sess.active = 1540+ selectTestAttachmentTabLocked(sess, 1)
543 sess.mu.Unlock()541 sess.mu.Unlock()
544 542
545 result := sendCommand(t, d, ports.CommandRequest{Slug: "close-tab", TargetSession: "work"})543 result := sendCommand(t, d, ports.CommandRequest{Slug: "close-tab", TargetSession: "work"})
@@ -560,7 +558,7 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
560 verify func(*testing.T, *Daemon, *session)558 verify func(*testing.T, *Daemon, *session)
561 }{559 }{
562 {name: "split focused pane", slug: "split-right", verify: func(t *testing.T, _ *Daemon, sess *session) {560 {name: "split focused pane", slug: "split-right", verify: func(t *testing.T, _ *Daemon, sess *session) {
563- tb := sess.activeTab()561+ tb := testAttachmentTab(sess)
564 tb.mu.Lock()562 tb.mu.Lock()
565 defer tb.mu.Unlock()563 defer tb.mu.Unlock()
566 require.Len(t, tb.panes, 2)564 require.Len(t, tb.panes, 2)
@@ -569,10 +567,10 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
569 sess.mu.Lock()567 sess.mu.Lock()
570 defer sess.mu.Unlock()568 defer sess.mu.Unlock()
571 require.Len(t, sess.tabs, 2)569 require.Len(t, sess.tabs, 2)
572- require.Equal(t, 1, sess.active)570+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess))
573 }},571 }},
574 {name: "rename tab", slug: "rename-tab", args: []string{"logs"}, verify: func(t *testing.T, _ *Daemon, sess *session) {572 {name: "rename tab", slug: "rename-tab", args: []string{"logs"}, verify: func(t *testing.T, _ *Daemon, sess *session) {
575- tb := sess.activeTab()573+ tb := testAttachmentTab(sess)
576 tb.mu.Lock()574 tb.mu.Lock()
577 defer tb.mu.Unlock()575 defer tb.mu.Unlock()
578 require.Equal(t, "logs", tb.name)576 require.Equal(t, "logs", tb.name)
@@ -762,7 +760,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
762 target.ctx, target.cancel = context.WithCancel(d.serveCtx)760 target.ctx, target.cancel = context.WithCancel(d.serveCtx)
763 sess.mu.Lock()761 sess.mu.Lock()
764 sess.tabs = append(sess.tabs, target)762 sess.tabs = append(sess.tabs, target)
765- sess.active = 0763+ selectTestAttachmentTabLocked(sess, 0)
766 sess.mu.Unlock()764 sess.mu.Unlock()
767 original := target.tree.Focus765 original := target.tree.Focus
768 766
@@ -808,7 +806,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
808 require.True(t, awaitCommandResult(t, secondSends).OK)806 require.True(t, awaitCommandResult(t, secondSends).OK)
809 807
810 sess.mu.Lock()808 sess.mu.Lock()
811- require.Zero(t, sess.active, "self commands must not select the invoking tab")809+ require.Zero(t, testAttachmentTabIndexLocked(sess), "self commands must not select the invoking tab")
812 sess.mu.Unlock()810 sess.mu.Unlock()
813 target.mu.Lock()811 target.mu.Lock()
814 defer target.mu.Unlock()812 defer target.mu.Unlock()
@@ -824,7 +822,7 @@ func TestHandleCommandMovePaneUsesActiveFocusedSourceWithSessionFlag(t *testing.
824 publishTiledPaneOwners(source, inactive)822 publishTiledPaneOwners(source, inactive)
825 source.mu.Lock()823 source.mu.Lock()
826 source.tabs = append(source.tabs, inactive)824 source.tabs = append(source.tabs, inactive)
827- source.active = 0825+ selectTestAttachmentTabLocked(source, 0)
828 source.mu.Unlock()826 source.mu.Unlock()
829 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")827 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
830 828
@@ -859,7 +857,7 @@ func TestHandleCommandMovePaneSelfUsesStableSourceIDs(t *testing.T) {
859 publishTiledPaneOwners(source, inactive)857 publishTiledPaneOwners(source, inactive)
860 source.mu.Lock()858 source.mu.Lock()
861 source.tabs = append(source.tabs, inactive)859 source.tabs = append(source.tabs, inactive)
862- source.active = 0860+ selectTestAttachmentTabLocked(source, 0)
863 source.mu.Unlock()861 source.mu.Unlock()
864 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")862 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
865 863
@@ -896,7 +894,7 @@ func TestHandleCommandMoveTabUsesActiveTabWithoutSelf(t *testing.T) {
896 publishTiledPaneOwners(source, second)894 publishTiledPaneOwners(source, second)
897 source.mu.Lock()895 source.mu.Lock()
898 source.tabs = append(source.tabs, second)896 source.tabs = append(source.tabs, second)
899- source.active = 1897+ selectTestAttachmentTabLocked(source, 1)
900 source.mu.Unlock()898 source.mu.Unlock()
901 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")899 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
902 900
@@ -923,7 +921,7 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
923 publishTiledPaneOwners(source, second)921 publishTiledPaneOwners(source, second)
924 source.mu.Lock()922 source.mu.Lock()
925 source.tabs = append(source.tabs, second)923 source.tabs = append(source.tabs, second)
926- source.active = 0924+ selectTestAttachmentTabLocked(source, 0)
927 source.mu.Unlock()925 source.mu.Unlock()
928 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")926 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
929 927
@@ -1093,7 +1091,7 @@ func TestHandleCommandMovePaneStableIDsLocateSessionWithoutSelfRedirect(t *testi
1093 publishTiledPaneOwners(source, inactive)1091 publishTiledPaneOwners(source, inactive)
1094 source.mu.Lock()1092 source.mu.Lock()
1095 source.tabs = append(source.tabs, inactive)1093 source.tabs = append(source.tabs, inactive)
1096- source.active = 11094+ selectTestAttachmentTabLocked(source, 1)
1097 source.mu.Unlock()1095 source.mu.Unlock()
1098 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1096 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1099 1097
@@ -1126,7 +1124,7 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
1126 left.mu.Lock()1124 left.mu.Lock()
1127 left.name = "left"1125 left.name = "left"
1128 left.tabs[0].stableID = "left-moved"1126 left.tabs[0].stableID = "left-moved"
1129- left.active = 01127+ selectTestAttachmentTabLocked(left, 0)
1130 left.mu.Unlock()1128 left.mu.Unlock()
1131 1129
1132 right := addMoveTabTestSession(d, "right", "right-stays")1130 right := addMoveTabTestSession(d, "right", "right-stays")
@@ -1135,7 +1133,7 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
1135 publishTiledPaneOwners(right, rightMoved)1133 publishTiledPaneOwners(right, rightMoved)
1136 right.mu.Lock()1134 right.mu.Lock()
1137 right.tabs = append(right.tabs, rightMoved)1135 right.tabs = append(right.tabs, rightMoved)
1138- right.active = 11136+ selectTestAttachmentTabLocked(right, 1)
1139 right.mu.Unlock()1137 right.mu.Unlock()
1140 1138
1141 leftFrame := commandFrame(t, ports.CommandRequest{1139 leftFrame := commandFrame(t, ports.CommandRequest{
@@ -1179,7 +1177,7 @@ func addNamedMoveDestination(d *Daemon, name, tabID, paneID string) *session {
1179 tb := newTabWithStableID(tabID, paneID, newQuietPTY(), domain.Size{Cols: 80, Rows: 23})1177 tb := newTabWithStableID(tabID, paneID, newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
1180 tb.ctx, tb.cancel = context.WithCancel(ctx)1178 tb.ctx, tb.cancel = context.WithCancel(ctx)
1181 sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, incarnation: domain.IncarnationID{5},1179 sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, incarnation: domain.IncarnationID{5},
1182- ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}, active: 0,1180+ ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb},
1183 }1181 }
1184 publishTiledPaneOwners(sess, tb)1182 publishTiledPaneOwners(sess, tb)
1185 d.mu.Lock()1183 d.mu.Lock()
M internal/usecase/daemon/copy_mouse_input.go +1 -1
@@ -179,7 +179,7 @@ func (d *Daemon) handleFreshCopyPointer(sess *session, ac *attachedClient, ev mo
179179 if !ok {
180180 return true
181181 }
182- if !d.publishCopyMode(sess, ac, sess.tabForAttachmentOrActive(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {
182+ if !d.publishCopyMode(sess, ac, sess.tabForAttachment(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {
183183 mode.StartCharacterSelection(pointer.press)
184184 mode.ExtendCharacterSelection(mapped.pos)
185185 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
@@ -179,7 +179,7 @@ func (d *Daemon) handleFreshCopyPointer(sess *session, ac *attachedClient, ev mo
179 if !ok {179 if !ok {
180 return true180 return true
181 }181 }
182- if !d.publishCopyMode(sess, ac, sess.tabForAttachmentOrActive(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {182+ if !d.publishCopyMode(sess, ac, sess.tabForAttachment(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {
183 mode.StartCharacterSelection(pointer.press)183 mode.StartCharacterSelection(pointer.press)
184 mode.ExtendCharacterSelection(mapped.pos)184 mode.ExtendCharacterSelection(mapped.pos)
185 }, func(runtime *overlayRuntime, mode *scopy.Mode) {185 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
M internal/usecase/daemon/copy_mouse_map_test.go +1 -1
@@ -71,7 +71,7 @@ func TestCopyMouseGeometryUsesClientFrameExactlyOnce(t *testing.T) {
7171 p, release := newBlockingPTY(t)
7272 defer release()
7373 d, sess, _, _ := newManualSessionWithPTYs(t, p)
74- tb := sess.activeTab()
74+ tb := testAttachmentTab(sess)
7575
7676 tb.mu.Lock()
7777 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 0, 1)
@@ -71,7 +71,7 @@ func TestCopyMouseGeometryUsesClientFrameExactlyOnce(t *testing.T) {
71 p, release := newBlockingPTY(t)71 p, release := newBlockingPTY(t)
72 defer release()72 defer release()
73 d, sess, _, _ := newManualSessionWithPTYs(t, p)73 d, sess, _, _ := newManualSessionWithPTYs(t, p)
74- tb := sess.activeTab()74+ tb := testAttachmentTab(sess)
75 75
76 tb.mu.Lock()76 tb.mu.Lock()
77 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 0, 1)77 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 0, 1)
M internal/usecase/daemon/copymode.go +2 -2
@@ -63,7 +63,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) {
6363 d.invalidateRender(sess, ac, true, "copymode.go")
6464 }
6565 func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) {
66- tb := sess.tabForAttachmentOrActive(ac)
66+ tb := sess.tabForAttachment(ac)
6767 if tb == nil {
6868 return
6969 }
@@ -114,7 +114,7 @@ func (d *Daemon) publishCopyMode(sess *session, ac *attachedClient, tb *tab, p *
114114 if d.beforeCopyModeRevalidate != nil {
115115 d.beforeCopyModeRevalidate()
116116 }
117- valid := sess.tabForAttachmentOrActive(ac) == tb
117+ valid := sess.tabForAttachment(ac) == tb
118118 if valid {
119119 tb.mu.Lock()
120120 valid = tb.panes[p.id] == p || (tb.floating.state == floatingVisible && tb.floating.pane == p)
@@ -63,7 +63,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) {
63 d.invalidateRender(sess, ac, true, "copymode.go")63 d.invalidateRender(sess, ac, true, "copymode.go")
64 }64 }
65 func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) {65 func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) {
66- tb := sess.tabForAttachmentOrActive(ac)66+ tb := sess.tabForAttachment(ac)
67 if tb == nil {67 if tb == nil {
68 return68 return
69 }69 }
@@ -114,7 +114,7 @@ func (d *Daemon) publishCopyMode(sess *session, ac *attachedClient, tb *tab, p *
114 if d.beforeCopyModeRevalidate != nil {114 if d.beforeCopyModeRevalidate != nil {
115 d.beforeCopyModeRevalidate()115 d.beforeCopyModeRevalidate()
116 }116 }
117- valid := sess.tabForAttachmentOrActive(ac) == tb117+ valid := sess.tabForAttachment(ac) == tb
118 if valid {118 if valid {
119 tb.mu.Lock()119 tb.mu.Lock()
120 valid = tb.panes[p.id] == p || (tb.floating.state == floatingVisible && tb.floating.pane == p)120 valid = tb.panes[p.id] == p || (tb.floating.state == floatingVisible && tb.floating.pane == p)
M internal/usecase/daemon/copymode_lifecycle_test.go +11 -11
@@ -45,8 +45,8 @@ func TestCopyModeLifecycleActivateTabClearsRuntime(t *testing.T) {
4545 seedCopyInteractionLocked(ac.overlays, ac.overlays.copyPane, ac.overlays.copyDocument)
4646 ac.overlays.copyMu.Unlock()
4747
48- require.True(t, sess.switchTab(1))
49- d.activateTab(sess, sess.activeTab())
48+ require.True(t, selectTestAttachmentTab(sess, 1))
49+ d.activateTab(sess, testAttachmentTab(sess))
5050
5151 require.False(t, ac.overlays.copyActive())
5252 ac.overlays.copyMu.Lock()
@@ -80,7 +80,7 @@ func TestActivateTabAfterResizePathAndCopyModeLifecycle(t *testing.T) {
8080
8181 d.enterCopyMode(sess, ac)
8282 require.True(t, ac.overlays.copyActive())
83- d.activateTabAfterResizeForLease(sess, sess.activeTab(), true, ac, lease)
83+ d.activateTabAfterResizeForLease(sess, testAttachmentTab(sess), true, ac, lease)
8484
8585 require.False(t, ac.overlays.copyActive())
8686 require.Equal(t, tt.leaseBacked, <-path)
@@ -90,7 +90,7 @@ func TestActivateTabAfterResizePathAndCopyModeLifecycle(t *testing.T) {
9090
9191 func TestCopyModeLifecycleClosePaneClearsRecoveredClientState(t *testing.T) {
9292 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
93- tb := sess.activeTab()
93+ tb := testAttachmentTab(sess)
9494 closingPTY := portsmocks.NewMockPTY(t)
9595 closingPTY.EXPECT().Close().Return(nil).Once()
9696 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -126,7 +126,7 @@ func TestCopyModeLifecycleCloseOnlyPaneInTabClearsRecoveredClientState(t *testin
126126 release()
127127 }
128128 }()
129- tb := sess.activeTab()
129+ tb := testAttachmentTab(sess)
130130 p := tb.focusedPane()
131131 d.enterCopyMode(sess, ac)
132132 require.True(t, ac.overlays.copyActive())
@@ -143,7 +143,7 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) {
143143 release()
144144 }
145145 }()
146- tb := sess.activeTab()
146+ tb := testAttachmentTab(sess)
147147 p := tb.focusedPane()
148148 p.mu.Lock()
149149 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
@@ -185,7 +185,7 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) {
185185 renderSnapshot := ac.overlays.SnapshotForRender()
186186 candidateWasRenderable := renderSnapshot.copyActive
187187 renderSnapshot.Unlock()
188- require.True(t, sess.switchTab(1))
188+ require.True(t, selectTestAttachmentTab(sess, 1))
189189 close(resume)
190190 resumed = true
191191
@@ -225,7 +225,7 @@ func TestCopyModeLifecyclePublicationEpochRejectsReleaseAndPaneClose(t *testing.
225225 release()
226226 }
227227 }()
228- tb := sess.activeTab()
228+ tb := testAttachmentTab(sess)
229229 p := tb.focusedPane()
230230
231231 // A real press is required: the motion publication must inherit this
@@ -285,7 +285,7 @@ func TestCopyModeLifecycleFloatingCloseDuringPublicationDoesNotResurrect(t *test
285285 release()
286286 }
287287 }()
288- tb := sess.activeTab()
288+ tb := testAttachmentTab(sess)
289289 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
290290 installTestFloating(tb, floating, true)
291291 tb.mu.Lock()
@@ -356,7 +356,7 @@ func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.
356356 {
357357 name: "inactive tab",
358358 invalidate: func(sess *session, _ *tab, _ *pane) {
359- require.True(t, sess.switchTab(1))
359+ require.True(t, selectTestAttachmentTab(sess, 1))
360360 },
361361 },
362362 } {
@@ -367,7 +367,7 @@ func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.
367367 release()
368368 }
369369 }()
370- tb := sess.activeTab()
370+ tb := testAttachmentTab(sess)
371371 p := tb.focusedPane()
372372 p.mu.Lock()
373373 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
@@ -45,8 +45,8 @@ func TestCopyModeLifecycleActivateTabClearsRuntime(t *testing.T) {
45 seedCopyInteractionLocked(ac.overlays, ac.overlays.copyPane, ac.overlays.copyDocument)45 seedCopyInteractionLocked(ac.overlays, ac.overlays.copyPane, ac.overlays.copyDocument)
46 ac.overlays.copyMu.Unlock()46 ac.overlays.copyMu.Unlock()
47 47
48- require.True(t, sess.switchTab(1))48+ require.True(t, selectTestAttachmentTab(sess, 1))
49- d.activateTab(sess, sess.activeTab())49+ d.activateTab(sess, testAttachmentTab(sess))
50 50
51 require.False(t, ac.overlays.copyActive())51 require.False(t, ac.overlays.copyActive())
52 ac.overlays.copyMu.Lock()52 ac.overlays.copyMu.Lock()
@@ -80,7 +80,7 @@ func TestActivateTabAfterResizePathAndCopyModeLifecycle(t *testing.T) {
80 80
81 d.enterCopyMode(sess, ac)81 d.enterCopyMode(sess, ac)
82 require.True(t, ac.overlays.copyActive())82 require.True(t, ac.overlays.copyActive())
83- d.activateTabAfterResizeForLease(sess, sess.activeTab(), true, ac, lease)83+ d.activateTabAfterResizeForLease(sess, testAttachmentTab(sess), true, ac, lease)
84 84
85 require.False(t, ac.overlays.copyActive())85 require.False(t, ac.overlays.copyActive())
86 require.Equal(t, tt.leaseBacked, <-path)86 require.Equal(t, tt.leaseBacked, <-path)
@@ -90,7 +90,7 @@ func TestActivateTabAfterResizePathAndCopyModeLifecycle(t *testing.T) {
90 90
91 func TestCopyModeLifecycleClosePaneClearsRecoveredClientState(t *testing.T) {91 func TestCopyModeLifecycleClosePaneClearsRecoveredClientState(t *testing.T) {
92 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})92 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
93- tb := sess.activeTab()93+ tb := testAttachmentTab(sess)
94 closingPTY := portsmocks.NewMockPTY(t)94 closingPTY := portsmocks.NewMockPTY(t)
95 closingPTY.EXPECT().Close().Return(nil).Once()95 closingPTY.EXPECT().Close().Return(nil).Once()
96 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}96 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -126,7 +126,7 @@ func TestCopyModeLifecycleCloseOnlyPaneInTabClearsRecoveredClientState(t *testin
126 release()126 release()
127 }127 }
128 }()128 }()
129- tb := sess.activeTab()129+ tb := testAttachmentTab(sess)
130 p := tb.focusedPane()130 p := tb.focusedPane()
131 d.enterCopyMode(sess, ac)131 d.enterCopyMode(sess, ac)
132 require.True(t, ac.overlays.copyActive())132 require.True(t, ac.overlays.copyActive())
@@ -143,7 +143,7 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) {
143 release()143 release()
144 }144 }
145 }()145 }()
146- tb := sess.activeTab()146+ tb := testAttachmentTab(sess)
147 p := tb.focusedPane()147 p := tb.focusedPane()
148 p.mu.Lock()148 p.mu.Lock()
149 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)149 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
@@ -185,7 +185,7 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) {
185 renderSnapshot := ac.overlays.SnapshotForRender()185 renderSnapshot := ac.overlays.SnapshotForRender()
186 candidateWasRenderable := renderSnapshot.copyActive186 candidateWasRenderable := renderSnapshot.copyActive
187 renderSnapshot.Unlock()187 renderSnapshot.Unlock()
188- require.True(t, sess.switchTab(1))188+ require.True(t, selectTestAttachmentTab(sess, 1))
189 close(resume)189 close(resume)
190 resumed = true190 resumed = true
191 191
@@ -225,7 +225,7 @@ func TestCopyModeLifecyclePublicationEpochRejectsReleaseAndPaneClose(t *testing.
225 release()225 release()
226 }226 }
227 }()227 }()
228- tb := sess.activeTab()228+ tb := testAttachmentTab(sess)
229 p := tb.focusedPane()229 p := tb.focusedPane()
230 230
231 // A real press is required: the motion publication must inherit this231 // A real press is required: the motion publication must inherit this
@@ -285,7 +285,7 @@ func TestCopyModeLifecycleFloatingCloseDuringPublicationDoesNotResurrect(t *test
285 release()285 release()
286 }286 }
287 }()287 }()
288- tb := sess.activeTab()288+ tb := testAttachmentTab(sess)
289 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})289 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
290 installTestFloating(tb, floating, true)290 installTestFloating(tb, floating, true)
291 tb.mu.Lock()291 tb.mu.Lock()
@@ -356,7 +356,7 @@ func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.
356 {356 {
357 name: "inactive tab",357 name: "inactive tab",
358 invalidate: func(sess *session, _ *tab, _ *pane) {358 invalidate: func(sess *session, _ *tab, _ *pane) {
359- require.True(t, sess.switchTab(1))359+ require.True(t, selectTestAttachmentTab(sess, 1))
360 },360 },
361 },361 },
362 } {362 } {
@@ -367,7 +367,7 @@ func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.
367 release()367 release()
368 }368 }
369 }()369 }()
370- tb := sess.activeTab()370+ tb := testAttachmentTab(sess)
371 p := tb.focusedPane()371 p := tb.focusedPane()
372 p.mu.Lock()372 p.mu.Lock()
373 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)373 document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
M internal/usecase/daemon/copymode_mouse_test.go +5 -5
@@ -26,7 +26,7 @@ func mouseCopyHarness(t *testing.T, rows ...string) (*Daemon, *session, *attache
2626 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
2727 clock := &manualMouseClock{now: time.Unix(1, 0)}
2828 d.clock = clock
29- pane := sess.activeTab().focusedPane()
29+ pane := testAttachmentTab(sess).focusedPane()
3030 for i, row := range rows {
3131 copy(pane.screen.Frame.Row(i), testRow(row))
3232 }
@@ -41,7 +41,7 @@ func copyMouseInput(d *Daemon, sess *session, ac *attachedClient, raw string) {
4141 func TestCopyDoubleClick(t *testing.T) {
4242 t.Run("same normalized position within interval selects word", func(t *testing.T) {
4343 d, sess, ac, clock := mouseCopyHarness(t, "a界 beta")
44- pane := sess.activeTab().focusedPane()
44+ pane := testAttachmentTab(sess).focusedPane()
4545 d.exitCopyMode(ac)
4646 pane.mu.Lock()
4747 row := pane.screen.Frame.Row(0)
@@ -324,7 +324,7 @@ func TestCopyWordSelectionYanksExactOSC52(t *testing.T) {
324324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
325325 clock := &manualMouseClock{now: time.Unix(1, 0)}
326326 d.clock = clock
327- copy(sess.activeTab().focusedPane().screen.Frame.Row(0), testRow("alpha beta"))
327+ copy(testAttachmentTab(sess).focusedPane().screen.Frame.Row(0), testRow("alpha beta"))
328328 d.enterCopyMode(sess, ac)
329329 mustOutputData(t, sends)
330330
@@ -348,11 +348,11 @@ func TestCopyPointerAndClickResetOnCopyExitAndReplacement(t *testing.T) {
348348 {name: "escape", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("\x1bx")) }},
349349 {name: "yank", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("y")) }},
350350 {name: "replacement publication", run: func(d *Daemon, sess *session, ac *attachedClient) {
351- p := sess.activeTab().focusedPane()
351+ p := testAttachmentTab(sess).focusedPane()
352352 p.mu.Lock()
353353 doc := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
354354 p.mu.Unlock()
355- require.True(t, d.publishCopyMode(sess, ac, sess.activeTab(), p, doc, nil, nil))
355+ require.True(t, d.publishCopyMode(sess, ac, testAttachmentTab(sess), p, doc, nil, nil))
356356 }},
357357 } {
358358 t.Run(tc.name, func(t *testing.T) {
@@ -26,7 +26,7 @@ func mouseCopyHarness(t *testing.T, rows ...string) (*Daemon, *session, *attache
26 d, sess, ac, _ := newManualSessionWithPTYs(t, p)26 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
27 clock := &manualMouseClock{now: time.Unix(1, 0)}27 clock := &manualMouseClock{now: time.Unix(1, 0)}
28 d.clock = clock28 d.clock = clock
29- pane := sess.activeTab().focusedPane()29+ pane := testAttachmentTab(sess).focusedPane()
30 for i, row := range rows {30 for i, row := range rows {
31 copy(pane.screen.Frame.Row(i), testRow(row))31 copy(pane.screen.Frame.Row(i), testRow(row))
32 }32 }
@@ -41,7 +41,7 @@ func copyMouseInput(d *Daemon, sess *session, ac *attachedClient, raw string) {
41 func TestCopyDoubleClick(t *testing.T) {41 func TestCopyDoubleClick(t *testing.T) {
42 t.Run("same normalized position within interval selects word", func(t *testing.T) {42 t.Run("same normalized position within interval selects word", func(t *testing.T) {
43 d, sess, ac, clock := mouseCopyHarness(t, "a界 beta")43 d, sess, ac, clock := mouseCopyHarness(t, "a界 beta")
44- pane := sess.activeTab().focusedPane()44+ pane := testAttachmentTab(sess).focusedPane()
45 d.exitCopyMode(ac)45 d.exitCopyMode(ac)
46 pane.mu.Lock()46 pane.mu.Lock()
47 row := pane.screen.Frame.Row(0)47 row := pane.screen.Frame.Row(0)
@@ -324,7 +324,7 @@ func TestCopyWordSelectionYanksExactOSC52(t *testing.T) {
324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
325 clock := &manualMouseClock{now: time.Unix(1, 0)}325 clock := &manualMouseClock{now: time.Unix(1, 0)}
326 d.clock = clock326 d.clock = clock
327- copy(sess.activeTab().focusedPane().screen.Frame.Row(0), testRow("alpha beta"))327+ copy(testAttachmentTab(sess).focusedPane().screen.Frame.Row(0), testRow("alpha beta"))
328 d.enterCopyMode(sess, ac)328 d.enterCopyMode(sess, ac)
329 mustOutputData(t, sends)329 mustOutputData(t, sends)
330 330
@@ -348,11 +348,11 @@ func TestCopyPointerAndClickResetOnCopyExitAndReplacement(t *testing.T) {
348 {name: "escape", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("\x1bx")) }},348 {name: "escape", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("\x1bx")) }},
349 {name: "yank", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("y")) }},349 {name: "yank", run: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("y")) }},
350 {name: "replacement publication", run: func(d *Daemon, sess *session, ac *attachedClient) {350 {name: "replacement publication", run: func(d *Daemon, sess *session, ac *attachedClient) {
351- p := sess.activeTab().focusedPane()351+ p := testAttachmentTab(sess).focusedPane()
352 p.mu.Lock()352 p.mu.Lock()
353 doc := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)353 doc := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), nil), domain.DefaultWordSeparators)
354 p.mu.Unlock()354 p.mu.Unlock()
355- require.True(t, d.publishCopyMode(sess, ac, sess.activeTab(), p, doc, nil, nil))355+ require.True(t, d.publishCopyMode(sess, ac, testAttachmentTab(sess), p, doc, nil, nil))
356 }},356 }},
357 } {357 } {
358 t.Run(tc.name, func(t *testing.T) {358 t.Run(tc.name, func(t *testing.T) {
M internal/usecase/daemon/copymode_test.go +8 -8
@@ -108,7 +108,7 @@ func TestCopySearchModalGeometry(t *testing.T) {
108108 func TestCopyModeDocumentCarriesPaneRowIDs(t *testing.T) {
109109 p, _ := newBlockingPTY(t)
110110 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
111- pane := sess.activeTab().focusedPane()
111+ pane := testAttachmentTab(sess).focusedPane()
112112 appendHistoryRow(t, pane.history, testRow("history"))
113113 pane.screen.Write([]byte("live"))
114114 historyID := pane.history.View().RowID(0)
@@ -128,7 +128,7 @@ func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) {
128128 p, release := newBlockingPTY(t)
129129 defer release()
130130 _, sess, _, _ := newManualSessionWithPTYs(t, p)
131- tb := sess.activeTab()
131+ tb := testAttachmentTab(sess)
132132 pane := tb.focusedPane()
133133 pane.mu.Lock()
134134 snap := scopy.NewSnapshot(pane.history, pane.screen.Frame, pane.screen.LineBounds(), nil)
@@ -162,7 +162,7 @@ func TestCopyModeFrameIncludesTopAndBottomChrome(t *testing.T) {
162162 _, sess, _, _ := newManualSessionWithPTYs(t, p)
163163 defer release()
164164 sess.name = "work"
165- tb := sess.activeTab()
165+ tb := testAttachmentTab(sess)
166166 tb.focusedPane().screen = vt.NewScreen(12, 3)
167167 tb.focusedPane().screen.Write([]byte("live"))
168168 snap := scopy.NewSnapshot(tb.focusedPane().history, tb.focusedPane().screen.Frame, tb.focusedPane().screen.LineBounds(), nil)
@@ -441,7 +441,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
441441 if sess == nil {
442442 return false
443443 }
444- win := sess.activeTab()
444+ win := testAttachmentTab(sess)
445445 if win == nil {
446446 return false
447447 }
@@ -662,7 +662,7 @@ func TestHandleCopyInputUsesImmutableSnapshotWithoutPaneLock(t *testing.T) {
662662 p, release := newBlockingPTY(t)
663663 defer release()
664664 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
665- pane := sess.activeTab().focusedPane()
665+ pane := testAttachmentTab(sess).focusedPane()
666666 pane.screen.Write([]byte("live"))
667667 d.enterCopyMode(sess, ac)
668668 awaitFrame(t, sends, ports.MsgOutput)
@@ -729,7 +729,7 @@ func installFloatingCopyFixture(t *testing.T, sess *session, size domain.Size) *
729729 floatingPTY, releaseFloating := newBlockingPTY(t)
730730 t.Cleanup(releaseFloating)
731731 fp := newPane("floating", floatingPTY, size)
732- installTestFloating(sess.activeTab(), fp, true)
732+ installTestFloating(testAttachmentTab(sess), fp, true)
733733 return fp
734734 }
735735
@@ -748,7 +748,7 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) {
748748 normal, releaseNormal := newBlockingPTYWithWrites(t, normalWrites)
749749 defer releaseNormal()
750750 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
751- tb := sess.activeTab()
751+ tb := testAttachmentTab(sess)
752752 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})
753753 tb.mu.Lock()
754754 tb.tree.Root = &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}
@@ -856,7 +856,7 @@ func TestFloatingExitClearsCopyModeBeforeRepaint(t *testing.T) {
856856 normal, releaseNormal := newBlockingPTY(t)
857857 defer releaseNormal()
858858 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
859- tb := sess.activeTab()
859+ tb := testAttachmentTab(sess)
860860 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
861861 fp.screen.Write([]byte("flt-live"))
862862
@@ -108,7 +108,7 @@ func TestCopySearchModalGeometry(t *testing.T) {
108 func TestCopyModeDocumentCarriesPaneRowIDs(t *testing.T) {108 func TestCopyModeDocumentCarriesPaneRowIDs(t *testing.T) {
109 p, _ := newBlockingPTY(t)109 p, _ := newBlockingPTY(t)
110 d, sess, ac, sends := newManualSessionWithPTYs(t, p)110 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
111- pane := sess.activeTab().focusedPane()111+ pane := testAttachmentTab(sess).focusedPane()
112 appendHistoryRow(t, pane.history, testRow("history"))112 appendHistoryRow(t, pane.history, testRow("history"))
113 pane.screen.Write([]byte("live"))113 pane.screen.Write([]byte("live"))
114 historyID := pane.history.View().RowID(0)114 historyID := pane.history.View().RowID(0)
@@ -128,7 +128,7 @@ func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) {
128 p, release := newBlockingPTY(t)128 p, release := newBlockingPTY(t)
129 defer release()129 defer release()
130 _, sess, _, _ := newManualSessionWithPTYs(t, p)130 _, sess, _, _ := newManualSessionWithPTYs(t, p)
131- tb := sess.activeTab()131+ tb := testAttachmentTab(sess)
132 pane := tb.focusedPane()132 pane := tb.focusedPane()
133 pane.mu.Lock()133 pane.mu.Lock()
134 snap := scopy.NewSnapshot(pane.history, pane.screen.Frame, pane.screen.LineBounds(), nil)134 snap := scopy.NewSnapshot(pane.history, pane.screen.Frame, pane.screen.LineBounds(), nil)
@@ -162,7 +162,7 @@ func TestCopyModeFrameIncludesTopAndBottomChrome(t *testing.T) {
162 _, sess, _, _ := newManualSessionWithPTYs(t, p)162 _, sess, _, _ := newManualSessionWithPTYs(t, p)
163 defer release()163 defer release()
164 sess.name = "work"164 sess.name = "work"
165- tb := sess.activeTab()165+ tb := testAttachmentTab(sess)
166 tb.focusedPane().screen = vt.NewScreen(12, 3)166 tb.focusedPane().screen = vt.NewScreen(12, 3)
167 tb.focusedPane().screen.Write([]byte("live"))167 tb.focusedPane().screen.Write([]byte("live"))
168 snap := scopy.NewSnapshot(tb.focusedPane().history, tb.focusedPane().screen.Frame, tb.focusedPane().screen.LineBounds(), nil)168 snap := scopy.NewSnapshot(tb.focusedPane().history, tb.focusedPane().screen.Frame, tb.focusedPane().screen.LineBounds(), nil)
@@ -441,7 +441,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
441 if sess == nil {441 if sess == nil {
442 return false442 return false
443 }443 }
444- win := sess.activeTab()444+ win := testAttachmentTab(sess)
445 if win == nil {445 if win == nil {
446 return false446 return false
447 }447 }
@@ -662,7 +662,7 @@ func TestHandleCopyInputUsesImmutableSnapshotWithoutPaneLock(t *testing.T) {
662 p, release := newBlockingPTY(t)662 p, release := newBlockingPTY(t)
663 defer release()663 defer release()
664 d, sess, ac, sends := newManualSessionWithPTYs(t, p)664 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
665- pane := sess.activeTab().focusedPane()665+ pane := testAttachmentTab(sess).focusedPane()
666 pane.screen.Write([]byte("live"))666 pane.screen.Write([]byte("live"))
667 d.enterCopyMode(sess, ac)667 d.enterCopyMode(sess, ac)
668 awaitFrame(t, sends, ports.MsgOutput)668 awaitFrame(t, sends, ports.MsgOutput)
@@ -729,7 +729,7 @@ func installFloatingCopyFixture(t *testing.T, sess *session, size domain.Size) *
729 floatingPTY, releaseFloating := newBlockingPTY(t)729 floatingPTY, releaseFloating := newBlockingPTY(t)
730 t.Cleanup(releaseFloating)730 t.Cleanup(releaseFloating)
731 fp := newPane("floating", floatingPTY, size)731 fp := newPane("floating", floatingPTY, size)
732- installTestFloating(sess.activeTab(), fp, true)732+ installTestFloating(testAttachmentTab(sess), fp, true)
733 return fp733 return fp
734 }734 }
735 735
@@ -748,7 +748,7 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) {
748 normal, releaseNormal := newBlockingPTYWithWrites(t, normalWrites)748 normal, releaseNormal := newBlockingPTYWithWrites(t, normalWrites)
749 defer releaseNormal()749 defer releaseNormal()
750 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)750 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
751- tb := sess.activeTab()751+ tb := testAttachmentTab(sess)
752 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})752 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})
753 tb.mu.Lock()753 tb.mu.Lock()
754 tb.tree.Root = &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}754 tb.tree.Root = &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}
@@ -856,7 +856,7 @@ func TestFloatingExitClearsCopyModeBeforeRepaint(t *testing.T) {
856 normal, releaseNormal := newBlockingPTY(t)856 normal, releaseNormal := newBlockingPTY(t)
857 defer releaseNormal()857 defer releaseNormal()
858 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)858 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
859- tb := sess.activeTab()859+ tb := testAttachmentTab(sess)
860 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})860 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
861 fp.screen.Write([]byte("flt-live"))861 fp.screen.Write([]byte("flt-live"))
862 862
M internal/usecase/daemon/daemon.go +1 -1
@@ -1140,7 +1140,7 @@ func (d *Daemon) handleHello(tr ports.Transport, f ports.Frame) {
11401140 return
11411141 }
11421142 if ac.proxied {
1143- meta, ok := sess.sessionMetaSnapshot()
1143+ meta, ok := sess.sessionMetaSnapshotFor(ac)
11441144 metaFrame, metaErr := frameSessionMeta(meta)
11451145 if !ok || metaErr != nil || ac.sendExpectedTransportForRole(postWelcomeToken.transport, metaFrame, postWelcomeTicket) != nil {
11461146 postWelcomeTicket.End()
@@ -1140,7 +1140,7 @@ func (d *Daemon) handleHello(tr ports.Transport, f ports.Frame) {
1140 return1140 return
1141 }1141 }
1142 if ac.proxied {1142 if ac.proxied {
1143- meta, ok := sess.sessionMetaSnapshot()1143+ meta, ok := sess.sessionMetaSnapshotFor(ac)
1144 metaFrame, metaErr := frameSessionMeta(meta)1144 metaFrame, metaErr := frameSessionMeta(meta)
1145 if !ok || metaErr != nil || ac.sendExpectedTransportForRole(postWelcomeToken.transport, metaFrame, postWelcomeTicket) != nil {1145 if !ok || metaErr != nil || ac.sendExpectedTransportForRole(postWelcomeToken.transport, metaFrame, postWelcomeTicket) != nil {
1146 postWelcomeTicket.End()1146 postWelcomeTicket.End()
M internal/usecase/daemon/daemon_test.go +93 -2
@@ -439,10 +439,101 @@ func newManualTabSession(t *testing.T, n int) (*Daemon, *session, *attachedClien
439439 return d, sess, ac, sends, releases
440440 }
441441
442-func activeTabIndex(sess *session) int {
442+func testAttachment(sess *session) *attachedClient {
443+ if sess == nil {
444+ return nil
445+ }
446+ attachments := sess.snapshotAttachments()
447+ if len(attachments) == 0 {
448+ return nil
449+ }
450+ return attachments[0]
451+}
452+
453+func testAttachmentTabLocked(sess *session) *tab {
454+ if sess == nil {
455+ return nil
456+ }
457+ var ac *attachedClient
458+ for candidate := range sess.attachments {
459+ ac = candidate
460+ break
461+ }
462+ if ac == nil {
463+ if len(sess.tabs) == 0 {
464+ return nil
465+ }
466+ return sess.tabs[0]
467+ }
468+ view := ac.viewSnapshot()
469+ for _, tb := range sess.tabs {
470+ if domain.TabStableID(tb.stableID) == view.tabID {
471+ return tb
472+ }
473+ }
474+ return nil
475+}
476+
477+func testAttachmentTab(sess *session) *tab {
478+ if ac := testAttachment(sess); ac != nil {
479+ return sess.tabForAttachment(ac)
480+ }
481+ return sess.firstTab()
482+}
483+
484+func selectTestAttachmentTab(sess *session, idx int) bool {
485+ ac := testAttachment(sess)
486+ if ac == nil {
487+ return idx == 0 && testAttachmentTab(sess) != nil
488+ }
489+ return sess.switchAttachmentTab(ac, idx)
490+}
491+
492+// selectTestAttachmentTabLocked is used only by fixtures that already hold
493+// sess.mu; production navigation always goes through selectAttachmentTab.
494+func selectTestAttachmentTabLocked(sess *session, idx int) bool {
495+ if sess == nil {
496+ return false
497+ }
498+ var ac *attachedClient
499+ for candidate := range sess.attachments {
500+ ac = candidate
501+ break
502+ }
503+ if ac == nil {
504+ return idx == 0 && len(sess.tabs) > 0
505+ }
506+ return sess.activateAttachmentViewLocked(ac, idx)
507+}
508+
509+func testAttachmentTabIndexLocked(sess *session) int {
510+ if sess == nil {
511+ return 0
512+ }
513+ var ac *attachedClient
514+ for candidate := range sess.attachments {
515+ ac = candidate
516+ break
517+ }
518+ if ac == nil {
519+ return 0
520+ }
521+ view := ac.viewSnapshot()
522+ for i, tb := range sess.tabs {
523+ if domain.TabStableID(tb.stableID) == view.tabID {
524+ return i
525+ }
526+ }
527+ return 0
528+}
529+
530+func testAttachmentTabIndex(sess *session) int {
531+ if sess == nil {
532+ return 0
533+ }
443534 sess.mu.Lock()
444535 defer sess.mu.Unlock()
445- return sess.active
536+ return testAttachmentTabIndexLocked(sess)
446537 }
447538
448539 func testRow(text string) []renderer.Cell {
@@ -439,10 +439,101 @@ func newManualTabSession(t *testing.T, n int) (*Daemon, *session, *attachedClien
439 return d, sess, ac, sends, releases439 return d, sess, ac, sends, releases
440 }440 }
441 441
442-func activeTabIndex(sess *session) int {442+func testAttachment(sess *session) *attachedClient {
443+ if sess == nil {
444+ return nil
445+ }
446+ attachments := sess.snapshotAttachments()
447+ if len(attachments) == 0 {
448+ return nil
449+ }
450+ return attachments[0]
451+}
452+
453+func testAttachmentTabLocked(sess *session) *tab {
454+ if sess == nil {
455+ return nil
456+ }
457+ var ac *attachedClient
458+ for candidate := range sess.attachments {
459+ ac = candidate
460+ break
461+ }
462+ if ac == nil {
463+ if len(sess.tabs) == 0 {
464+ return nil
465+ }
466+ return sess.tabs[0]
467+ }
468+ view := ac.viewSnapshot()
469+ for _, tb := range sess.tabs {
470+ if domain.TabStableID(tb.stableID) == view.tabID {
471+ return tb
472+ }
473+ }
474+ return nil
475+}
476+
477+func testAttachmentTab(sess *session) *tab {
478+ if ac := testAttachment(sess); ac != nil {
479+ return sess.tabForAttachment(ac)
480+ }
481+ return sess.firstTab()
482+}
483+
484+func selectTestAttachmentTab(sess *session, idx int) bool {
485+ ac := testAttachment(sess)
486+ if ac == nil {
487+ return idx == 0 && testAttachmentTab(sess) != nil
488+ }
489+ return sess.switchAttachmentTab(ac, idx)
490+}
491+
492+// selectTestAttachmentTabLocked is used only by fixtures that already hold
493+// sess.mu; production navigation always goes through selectAttachmentTab.
494+func selectTestAttachmentTabLocked(sess *session, idx int) bool {
495+ if sess == nil {
496+ return false
497+ }
498+ var ac *attachedClient
499+ for candidate := range sess.attachments {
500+ ac = candidate
501+ break
502+ }
503+ if ac == nil {
504+ return idx == 0 && len(sess.tabs) > 0
505+ }
506+ return sess.activateAttachmentViewLocked(ac, idx)
507+}
508+
509+func testAttachmentTabIndexLocked(sess *session) int {
510+ if sess == nil {
511+ return 0
512+ }
513+ var ac *attachedClient
514+ for candidate := range sess.attachments {
515+ ac = candidate
516+ break
517+ }
518+ if ac == nil {
519+ return 0
520+ }
521+ view := ac.viewSnapshot()
522+ for i, tb := range sess.tabs {
523+ if domain.TabStableID(tb.stableID) == view.tabID {
524+ return i
525+ }
526+ }
527+ return 0
528+}
529+
530+func testAttachmentTabIndex(sess *session) int {
531+ if sess == nil {
532+ return 0
533+ }
443 sess.mu.Lock()534 sess.mu.Lock()
444 defer sess.mu.Unlock()535 defer sess.mu.Unlock()
445- return sess.active536+ return testAttachmentTabIndexLocked(sess)
446 }537 }
447 538
448 func testRow(text string) []renderer.Cell {539 func testRow(text string) []renderer.Cell {
M internal/usecase/daemon/floating.go +4 -1
@@ -223,7 +223,10 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
223223 if d == nil || sess == nil {
224224 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active session", nil)
225225 }
226- tb := sess.tabForAttachmentOrActive(ac)
226+ tb := sess.tabForAttachment(ac)
227+ if ac == nil {
228+ tb = sess.firstTab()
229+ }
227230 if tb == nil {
228231 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active tab", layout.ErrNotFound)
229232 }
@@ -223,7 +223,10 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
223 if d == nil || sess == nil {223 if d == nil || sess == nil {
224 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active session", nil)224 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active session", nil)
225 }225 }
226- tb := sess.tabForAttachmentOrActive(ac)226+ tb := sess.tabForAttachment(ac)
227+ if ac == nil {
228+ tb = sess.firstTab()
229+ }
227 if tb == nil {230 if tb == nil {
228 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active tab", layout.ErrNotFound)231 return domain.UserErr(domain.NoticeFloatingSpawn, "couldn't open floating pane: no active tab", layout.ErrNotFound)
229 }232 }
M internal/usecase/daemon/floating_test.go +1 -1
@@ -485,7 +485,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
485485 t.Helper()
486486 normalPTY, releaseNormal := newBlockingPTY(t)
487487 d, sess, ac, sends := newManualSessionWithPTYs(t, normalPTY)
488- tb := sess.activeTab()
488+ tb := testAttachmentTab(sess)
489489 require.NotNil(t, tb)
490490 tb.mu.Lock()
491491 normal := tb.focusedPane()
@@ -485,7 +485,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
485 t.Helper()485 t.Helper()
486 normalPTY, releaseNormal := newBlockingPTY(t)486 normalPTY, releaseNormal := newBlockingPTY(t)
487 d, sess, ac, sends := newManualSessionWithPTYs(t, normalPTY)487 d, sess, ac, sends := newManualSessionWithPTYs(t, normalPTY)
488- tb := sess.activeTab()488+ tb := testAttachmentTab(sess)
489 require.NotNil(t, tb)489 require.NotNil(t, tb)
490 tb.mu.Lock()490 tb.mu.Lock()
491 normal := tb.focusedPane()491 normal := tb.focusedPane()
M internal/usecase/daemon/input.go +4 -4
@@ -93,7 +93,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) {
9393 invalidateRejectedLeftPointer(rt, ev)
9494 return
9595 }
96- tb := sess.tabForAttachmentOrActive(ac)
96+ tb := sess.tabForAttachment(ac)
9797 if tb == nil {
9898 invalidateRejectedLeftPointer(rt, ev)
9999 return
@@ -364,7 +364,7 @@ func (h daemonKeyHandler) Forward(data []byte) {
364364 if owned {
365365 defer effect.End()
366366 }
367- tb := sess.tabForAttachmentOrActive(h.ac)
367+ tb := sess.tabForAttachment(h.ac)
368368 if tb == nil {
369369 return
370370 }
@@ -409,7 +409,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) {
409409 case keys.ActionJumpAttention:
410410 if !proxiedJumpSearchesOtherSessions(h.ac.proxied) {
411411 if idx, ok := oldestAttentionTab(sess); ok && sess.switchAttachmentTab(h.ac, idx) {
412- h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(h.ac), false, h.ac, nil)
412+ h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(h.ac), false, h.ac, nil)
413413 h.d.invalidateRender(sess, h.ac, true, "input.go")
414414 }
415415 return
@@ -458,7 +458,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) {
458458 keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9:
459459 idx := int(action - keys.ActionSwitchTab1)
460460 if sess.switchAttachmentTab(h.ac, idx) {
461- h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(h.ac), false, h.ac, nil)
461+ h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(h.ac), false, h.ac, nil)
462462 h.d.invalidateRender(sess, h.ac, true, "input.go")
463463 }
464464 }
@@ -93,7 +93,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) {
93 invalidateRejectedLeftPointer(rt, ev)93 invalidateRejectedLeftPointer(rt, ev)
94 return94 return
95 }95 }
96- tb := sess.tabForAttachmentOrActive(ac)96+ tb := sess.tabForAttachment(ac)
97 if tb == nil {97 if tb == nil {
98 invalidateRejectedLeftPointer(rt, ev)98 invalidateRejectedLeftPointer(rt, ev)
99 return99 return
@@ -364,7 +364,7 @@ func (h daemonKeyHandler) Forward(data []byte) {
364 if owned {364 if owned {
365 defer effect.End()365 defer effect.End()
366 }366 }
367- tb := sess.tabForAttachmentOrActive(h.ac)367+ tb := sess.tabForAttachment(h.ac)
368 if tb == nil {368 if tb == nil {
369 return369 return
370 }370 }
@@ -409,7 +409,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) {
409 case keys.ActionJumpAttention:409 case keys.ActionJumpAttention:
410 if !proxiedJumpSearchesOtherSessions(h.ac.proxied) {410 if !proxiedJumpSearchesOtherSessions(h.ac.proxied) {
411 if idx, ok := oldestAttentionTab(sess); ok && sess.switchAttachmentTab(h.ac, idx) {411 if idx, ok := oldestAttentionTab(sess); ok && sess.switchAttachmentTab(h.ac, idx) {
412- h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(h.ac), false, h.ac, nil)412+ h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(h.ac), false, h.ac, nil)
413 h.d.invalidateRender(sess, h.ac, true, "input.go")413 h.d.invalidateRender(sess, h.ac, true, "input.go")
414 }414 }
415 return415 return
@@ -458,7 +458,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) {
458 keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9:458 keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9:
459 idx := int(action - keys.ActionSwitchTab1)459 idx := int(action - keys.ActionSwitchTab1)
460 if sess.switchAttachmentTab(h.ac, idx) {460 if sess.switchAttachmentTab(h.ac, idx) {
461- h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachmentOrActive(h.ac), false, h.ac, nil)461+ h.d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(h.ac), false, h.ac, nil)
462 h.d.invalidateRender(sess, h.ac, true, "input.go")462 h.d.invalidateRender(sess, h.ac, true, "input.go")
463 }463 }
464 }464 }
M internal/usecase/daemon/input_test.go +40 -40
@@ -197,7 +197,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
197197 "pane-1": {}, "pane-2": {}, "pane-3": {},
198198 }
199199 d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"])
200- tb := sess.activeTab()
200+ tb := testAttachmentTab(sess)
201201 p2 := newPane("pane-2", ptys["pane-2"], tb.size)
202202 p3 := newPane("pane-3", ptys["pane-3"], tb.size)
203203 tb.mu.Lock()
@@ -401,7 +401,7 @@ func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) {
401401 // Use the real key action: it requests the mandatory complete repaint for
402402 // the first target-tab frame.
403403 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2, nil)
404- require.Equal(t, 1, activeTabIndex(sess))
404+ require.Equal(t, 1, testAttachmentTabIndex(sess))
405405
406406 second := awaitFrame(t, sends, ports.MsgOutput)
407407 secondOutput, err := ports.UnmarshalOutput(second.Payload)
@@ -496,7 +496,7 @@ func TestAltFToggleRetainedFloatingPaneRepaintsImmediately(t *testing.T) {
496496 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})
497497 floating.ctx = floatingCtx
498498 floating.screen.Write([]byte("popup-content"))
499- installTestFloating(sess.activeTab(), floating, false)
499+ installTestFloating(testAttachmentTab(sess), floating, false)
500500
501501 // Establish the client shadow while the retained popup is hidden.
502502 d.paint(sess, ac, true, nil)
@@ -518,7 +518,7 @@ func TestAltFToggleRetainedFloatingPaneRepaintsImmediately(t *testing.T) {
518518 d.handleInput(sess, ac, []byte("\x1bf"))
519519 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
520520 require.NotContains(t, strings.Join(frameRows(client.Frame), "\n"), "popup-content")
521- tb := sess.activeTab()
521+ tb := testAttachmentTab(sess)
522522 tb.mu.Lock()
523523 require.Equal(t, floatingHidden, tb.floating.state)
524524 require.Same(t, floating, tb.floating.pane)
@@ -586,7 +586,7 @@ func TestFloatingKeyboardRoutesVisibleAndHiddenInput(t *testing.T) {
586586 defer releaseNormal()
587587 defer releaseFloating()
588588 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
589- installTestFloating(sess.activeTab(), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), tc.visible)
589+ installTestFloating(testAttachmentTab(sess), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), tc.visible)
590590
591591 daemonKeyHandler{d: d, ac: ac}.Forward(tc.input)
592592 if tc.visible {
@@ -608,7 +608,7 @@ func TestFloatingBracketedPasteAndGlobalActions(t *testing.T) {
608608 defer releaseNormal()
609609 defer releaseFloating()
610610 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
611- installTestFloating(sess.activeTab(), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), true)
611+ installTestFloating(testAttachmentTab(sess), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), true)
612612
613613 paste := []byte("\x1b[200~paste\x1bf\x1b[201~")
614614 d.handleInput(sess, ac, paste)
@@ -626,7 +626,7 @@ func TestFloatingStaysTerminalTargetAfterDirectionalFocus(t *testing.T) {
626626 defer releaseNormal()
627627 defer releaseFloating()
628628 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
629- tb := sess.activeTab()
629+ tb := testAttachmentTab(sess)
630630 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})
631631 tb.mu.Lock()
632632 tb.size = domain.Size{Cols: 81, Rows: 5}
@@ -658,7 +658,7 @@ func TestActionFocusPaneAtEdgeStaysSilent(t *testing.T) {
658658 // the user as a notice instead of being silently discarded.
659659 func TestActionFocusPaneGenuineErrorReportsNoticeInternal(t *testing.T) {
660660 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
661- tb := sess.activeTab()
661+ tb := testAttachmentTab(sess)
662662 tb.mu.Lock()
663663 tb.tree = nil
664664 tb.mu.Unlock()
@@ -675,7 +675,7 @@ func TestActionFocusPaneGenuineErrorReportsNoticeInternal(t *testing.T) {
675675
676676 func TestFloatingVisibilityRemainsIndependentAcrossTabSwitches(t *testing.T) {
677677 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
678- first := sess.activeTab()
678+ first := testAttachmentTab(sess)
679679 second := newTab(nil, first.size)
680680 installTestFloating(first, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), true)
681681 installTestFloating(second, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
@@ -699,7 +699,7 @@ func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) {
699699 floatingPTY := portsmocks.NewMockPTY(t)
700700 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
701701 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
702- tb := sess.activeTab()
702+ tb := testAttachmentTab(sess)
703703 tb.mu.Lock()
704704 tb.size = domain.Size{Cols: 100, Rows: 20}
705705 tb.mu.Unlock()
@@ -716,7 +716,7 @@ func TestFloatingMouseIgnoresBorderAndOutside(t *testing.T) {
716716 normal := portsmocks.NewMockPTY(t)
717717 floatingPTY := portsmocks.NewMockPTY(t)
718718 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
719- tb := sess.activeTab()
719+ tb := testAttachmentTab(sess)
720720 tb.mu.Lock()
721721 tb.size = domain.Size{Cols: 100, Rows: 20}
722722 tb.mu.Unlock()
@@ -736,7 +736,7 @@ func TestResponsiveDrawerFloatingMouseRoutesOnlyInnerClientCells(t *testing.T) {
736736 floatingPTY := portsmocks.NewMockPTY(t)
737737 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
738738 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
739- tb := sess.activeTab()
739+ tb := testAttachmentTab(sess)
740740 complete := domain.Size{Cols: 79, Rows: 25}
741741 content := tabSize(complete)
742742 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())
@@ -760,7 +760,7 @@ func TestResponsiveDrawerCopyMouseMapsInnerAndPinsDragAwayFromChrome(t *testing.
760760 p, release := newBlockingPTY(t)
761761 defer release()
762762 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
763- tb := sess.activeTab()
763+ tb := testAttachmentTab(sess)
764764 complete := domain.Size{Cols: 79, Rows: 25}
765765 content := tabSize(complete)
766766 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())
@@ -960,12 +960,12 @@ func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) {
960960 release()
961961 }
962962 }()
963- sess.active = tc.start
963+ selectTestAttachmentTab(sess, tc.start)
964964
965965 d.handleInput(sess, ac, []byte("\x1b "))
966966 d.handleInput(sess, ac, tc.query)
967967
968- require.Equal(t, tc.wantIndex, activeTabIndex(sess))
968+ require.Equal(t, tc.wantIndex, testAttachmentTabIndex(sess))
969969 })
970970 }
971971 }
@@ -1200,14 +1200,14 @@ func TestAltXClosesActiveTabAndSelectsRemaining(t *testing.T) {
12001200 p2, releasePTY2 := newBlockingPTY(t)
12011201 p3, releasePTY3 := newBlockingPTYWithWrites(t, writes)
12021202 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2, p3)
1203- sess.active = 1
1203+ selectTestAttachmentTab(sess, 1)
12041204
12051205 d.handleInput(sess, ac, []byte("\x1b "))
12061206 d.handleInput(sess, ac, []byte("CLT\r"))
12071207
12081208 require.Equal(t, 1, sessionCount(d))
12091209 require.Len(t, sess.tabs, 2)
1210- require.Equal(t, 1, activeTabIndex(sess), "closing middle tab selects the next remaining tab")
1210+ require.Equal(t, 1, testAttachmentTabIndex(sess), "closing middle tab selects the next remaining tab")
12111211 d.handleInput(sess, ac, []byte("Z"))
12121212 require.Eventually(t, func() bool { return len(writes) == 1 }, 2*time.Second, 5*time.Millisecond)
12131213 requirePTYWrite(t, writes, []byte("Z"))
@@ -1258,7 +1258,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) {
12581258 release()
12591259 }
12601260 }()
1261- sess.active = 1
1261+ selectTestAttachmentTab(sess, 1)
12621262
12631263 d.handleInput(sess, ac, []byte("\x1b "))
12641264 awaitFrame(t, sends, ports.MsgOutput)
@@ -1639,7 +1639,7 @@ func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing
16391639 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()
16401640 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
16411641 d.procComm = nil
1642- tb := sess.activeTab()
1642+ tb := testAttachmentTab(sess)
16431643 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
16441644 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
16451645 tb.mu.Lock()
@@ -1663,7 +1663,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
16631663 {
16641664 name: "split divider",
16651665 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {
1666- tb := sess.activeTab()
1666+ tb := testAttachmentTab(sess)
16671667 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
16681668 tb.mu.Lock()
16691669 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -1676,7 +1676,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
16761676 {
16771677 name: "split title bar",
16781678 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {
1679- tb := sess.activeTab()
1679+ tb := testAttachmentTab(sess)
16801680 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
16811681 tb.mu.Lock()
16821682 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -1689,7 +1689,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
16891689 {
16901690 name: "floating exterior",
16911691 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, press *[]byte) []byte {
1692- tb := sess.activeTab()
1692+ tb := testAttachmentTab(sess)
16931693 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
16941694 installTestFloating(tb, floating, true)
16951695 tb.mu.Lock()
@@ -1749,7 +1749,7 @@ func TestPressAtFormerStackTitleRowIsTreatedAsExpandedContent(t *testing.T) {
17491749 p, release := newBlockingPTY(t)
17501750 defer release()
17511751 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
1752- tb := sess.activeTab()
1752+ tb := testAttachmentTab(sess)
17531753 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 9})
17541754 p3 := newPane("pane-3", nil, domain.Size{Cols: 20, Rows: 1})
17551755 tb.mu.Lock()
@@ -1821,7 +1821,7 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) {
18211821 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()
18221822 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
18231823 d.procComm = nil
1824- tb := sess.activeTab()
1824+ tb := testAttachmentTab(sess)
18251825 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
18261826 tb.mu.Lock()
18271827 tb.size = tc.size
@@ -1844,7 +1844,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) {
18441844 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe()
18451845 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
18461846 d.procComm = nil
1847- tb := sess.activeTab()
1847+ tb := testAttachmentTab(sess)
18481848 for i := range 10 {
18491849 copy(tb.focusedPane().screen.Frame.Row(i), testRow(string(rune('a'+i))))
18501850 }
@@ -1878,7 +1878,7 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) {
18781878 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
18791879 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
18801880 d.procComm = nil
1881- tb := sess.activeTab()
1881+ tb := testAttachmentTab(sess)
18821882 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
18831883 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
18841884 tb.mu.Lock()
@@ -1905,7 +1905,7 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) {
19051905 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe()
19061906 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
19071907 d.procComm = nil
1908- tb := sess.activeTab()
1908+ tb := testAttachmentTab(sess)
19091909 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
19101910 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
19111911 tb.mu.Lock()
@@ -1935,7 +1935,7 @@ func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) {
19351935 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe()
19361936 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
19371937 d.procComm = nil
1938- tb := sess.activeTab()
1938+ tb := testAttachmentTab(sess)
19391939 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})
19401940 tb.mu.Lock()
19411941 tb.size = domain.Size{Cols: 20, Rows: 5}
@@ -1954,7 +1954,7 @@ func TestActiveCopyPressOnOtherPaneFocusesAndExitsBeforeFutureInput(t *testing.T
19541954 p1, release := newBlockingPTY(t)
19551955 defer release()
19561956 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1957- tb := sess.activeTab()
1957+ tb := testAttachmentTab(sess)
19581958 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
19591959 tb.mu.Lock()
19601960 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -2053,8 +2053,8 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
20532053 name: "press-owned release rejects mapping",
20542054 prepare: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) mouse.Event {
20552055 t.Helper()
2056- p := sess.activeTab().focusedPane()
2057- tb := sess.activeTab()
2056+ p := testAttachmentTab(sess).focusedPane()
2057+ tb := testAttachmentTab(sess)
20582058 tb.mu.Lock()
20592059 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 1, 2)
20602060 tb.mu.Unlock()
@@ -2069,7 +2069,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
20692069 name: "current geometry disappears",
20702070 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {
20712071 t.Helper()
2072- tb := sess.activeTab()
2072+ tb := testAttachmentTab(sess)
20732073 tb.mu.Lock()
20742074 tb.tree = nil
20752075 tb.mu.Unlock()
@@ -2080,7 +2080,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
20802080 name: "fresh press rejects document mapping",
20812081 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {
20822082 t.Helper()
2083- tb := sess.activeTab()
2083+ tb := testAttachmentTab(sess)
20842084 tb.mu.Lock()
20852085 tb.size.Rows = 40 // valid layout coordinate, outside the old document.
20862086 tb.mu.Unlock()
@@ -2109,7 +2109,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
21092109 }
21102110 defer func() { d.beforeCopyMouseMap = nil }()
21112111
2112- d.handleActiveCopyMouse(sess, ac, sess.activeTab(), ev)
2112+ d.handleActiveCopyMouse(sess, ac, testAttachmentTab(sess), ev)
21132113
21142114 ac.overlays.copyMu.Lock()
21152115 defer ac.overlays.copyMu.Unlock()
@@ -2128,7 +2128,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T)
21282128 p, release := newBlockingPTY(t)
21292129 defer release()
21302130 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
2131- pane := sess.activeTab().focusedPane()
2131+ pane := testAttachmentTab(sess).focusedPane()
21322132 for range 8 {
21332133 appendHistoryRow(t, pane.history, testRow("history"))
21342134 }
@@ -2162,13 +2162,13 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
21622162 {
21632163 name: "mono",
21642164 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {
2165- return sess.activeTab().focusedPane(), mouse.Event{Button: mouse.Left, Type: mouse.Press, Col: 1, Row: 1}
2165+ return testAttachmentTab(sess).focusedPane(), mouse.Event{Button: mouse.Left, Type: mouse.Press, Col: 1, Row: 1}
21662166 },
21672167 },
21682168 {
21692169 name: "split",
21702170 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {
2171- tb := sess.activeTab()
2171+ tb := testAttachmentTab(sess)
21722172 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
21732173 tb.mu.Lock()
21742174 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -2182,8 +2182,8 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
21822182 name: "floating",
21832183 setup: func(t *testing.T, d *Daemon, sess *session) (*pane, mouse.Event) {
21842184 p := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
2185- installTestFloating(sess.activeTab(), p, true)
2186- tb := sess.activeTab()
2185+ installTestFloating(testAttachmentTab(sess), p, true)
2186+ tb := testAttachmentTab(sess)
21872187 tb.mu.Lock()
21882188 _, geometry, visible := tb.visibleFloatingSnapshotLocked(d.currentFloatingConfig())
21892189 tb.mu.Unlock()
@@ -2197,7 +2197,7 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
21972197 defer release()
21982198 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
21992199 wantPane, ev := tc.setup(t, d, sess)
2200- tb := sess.activeTab()
2200+ tb := testAttachmentTab(sess)
22012201 tb.mu.Lock()
22022202 wantGeometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), ev.Col, ev.Row)
22032203 tb.mu.Unlock()
@@ -197,7 +197,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
197 "pane-1": {}, "pane-2": {}, "pane-3": {},197 "pane-1": {}, "pane-2": {}, "pane-3": {},
198 }198 }
199 d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"])199 d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"])
200- tb := sess.activeTab()200+ tb := testAttachmentTab(sess)
201 p2 := newPane("pane-2", ptys["pane-2"], tb.size)201 p2 := newPane("pane-2", ptys["pane-2"], tb.size)
202 p3 := newPane("pane-3", ptys["pane-3"], tb.size)202 p3 := newPane("pane-3", ptys["pane-3"], tb.size)
203 tb.mu.Lock()203 tb.mu.Lock()
@@ -401,7 +401,7 @@ func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) {
401 // Use the real key action: it requests the mandatory complete repaint for401 // Use the real key action: it requests the mandatory complete repaint for
402 // the first target-tab frame.402 // the first target-tab frame.
403 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2, nil)403 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2, nil)
404- require.Equal(t, 1, activeTabIndex(sess))404+ require.Equal(t, 1, testAttachmentTabIndex(sess))
405 405
406 second := awaitFrame(t, sends, ports.MsgOutput)406 second := awaitFrame(t, sends, ports.MsgOutput)
407 secondOutput, err := ports.UnmarshalOutput(second.Payload)407 secondOutput, err := ports.UnmarshalOutput(second.Payload)
@@ -496,7 +496,7 @@ func TestAltFToggleRetainedFloatingPaneRepaintsImmediately(t *testing.T) {
496 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})496 floating := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 5})
497 floating.ctx = floatingCtx497 floating.ctx = floatingCtx
498 floating.screen.Write([]byte("popup-content"))498 floating.screen.Write([]byte("popup-content"))
499- installTestFloating(sess.activeTab(), floating, false)499+ installTestFloating(testAttachmentTab(sess), floating, false)
500 500
501 // Establish the client shadow while the retained popup is hidden.501 // Establish the client shadow while the retained popup is hidden.
502 d.paint(sess, ac, true, nil)502 d.paint(sess, ac, true, nil)
@@ -518,7 +518,7 @@ func TestAltFToggleRetainedFloatingPaneRepaintsImmediately(t *testing.T) {
518 d.handleInput(sess, ac, []byte("\x1bf"))518 d.handleInput(sess, ac, []byte("\x1bf"))
519 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))519 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
520 require.NotContains(t, strings.Join(frameRows(client.Frame), "\n"), "popup-content")520 require.NotContains(t, strings.Join(frameRows(client.Frame), "\n"), "popup-content")
521- tb := sess.activeTab()521+ tb := testAttachmentTab(sess)
522 tb.mu.Lock()522 tb.mu.Lock()
523 require.Equal(t, floatingHidden, tb.floating.state)523 require.Equal(t, floatingHidden, tb.floating.state)
524 require.Same(t, floating, tb.floating.pane)524 require.Same(t, floating, tb.floating.pane)
@@ -586,7 +586,7 @@ func TestFloatingKeyboardRoutesVisibleAndHiddenInput(t *testing.T) {
586 defer releaseNormal()586 defer releaseNormal()
587 defer releaseFloating()587 defer releaseFloating()
588 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)588 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
589- installTestFloating(sess.activeTab(), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), tc.visible)589+ installTestFloating(testAttachmentTab(sess), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), tc.visible)
590 590
591 daemonKeyHandler{d: d, ac: ac}.Forward(tc.input)591 daemonKeyHandler{d: d, ac: ac}.Forward(tc.input)
592 if tc.visible {592 if tc.visible {
@@ -608,7 +608,7 @@ func TestFloatingBracketedPasteAndGlobalActions(t *testing.T) {
608 defer releaseNormal()608 defer releaseNormal()
609 defer releaseFloating()609 defer releaseFloating()
610 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)610 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
611- installTestFloating(sess.activeTab(), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), true)611+ installTestFloating(testAttachmentTab(sess), newPane("floating", floating, domain.Size{Cols: 20, Rows: 5}), true)
612 612
613 paste := []byte("\x1b[200~paste\x1bf\x1b[201~")613 paste := []byte("\x1b[200~paste\x1bf\x1b[201~")
614 d.handleInput(sess, ac, paste)614 d.handleInput(sess, ac, paste)
@@ -626,7 +626,7 @@ func TestFloatingStaysTerminalTargetAfterDirectionalFocus(t *testing.T) {
626 defer releaseNormal()626 defer releaseNormal()
627 defer releaseFloating()627 defer releaseFloating()
628 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)628 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
629- tb := sess.activeTab()629+ tb := testAttachmentTab(sess)
630 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})630 second := newPane("pane-2", nil, domain.Size{Cols: 40, Rows: 5})
631 tb.mu.Lock()631 tb.mu.Lock()
632 tb.size = domain.Size{Cols: 81, Rows: 5}632 tb.size = domain.Size{Cols: 81, Rows: 5}
@@ -658,7 +658,7 @@ func TestActionFocusPaneAtEdgeStaysSilent(t *testing.T) {
658 // the user as a notice instead of being silently discarded.658 // the user as a notice instead of being silently discarded.
659 func TestActionFocusPaneGenuineErrorReportsNoticeInternal(t *testing.T) {659 func TestActionFocusPaneGenuineErrorReportsNoticeInternal(t *testing.T) {
660 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)660 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
661- tb := sess.activeTab()661+ tb := testAttachmentTab(sess)
662 tb.mu.Lock()662 tb.mu.Lock()
663 tb.tree = nil663 tb.tree = nil
664 tb.mu.Unlock()664 tb.mu.Unlock()
@@ -675,7 +675,7 @@ func TestActionFocusPaneGenuineErrorReportsNoticeInternal(t *testing.T) {
675 675
676 func TestFloatingVisibilityRemainsIndependentAcrossTabSwitches(t *testing.T) {676 func TestFloatingVisibilityRemainsIndependentAcrossTabSwitches(t *testing.T) {
677 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)677 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
678- first := sess.activeTab()678+ first := testAttachmentTab(sess)
679 second := newTab(nil, first.size)679 second := newTab(nil, first.size)
680 installTestFloating(first, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), true)680 installTestFloating(first, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), true)
681 installTestFloating(second, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)681 installTestFloating(second, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
@@ -699,7 +699,7 @@ func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) {
699 floatingPTY := portsmocks.NewMockPTY(t)699 floatingPTY := portsmocks.NewMockPTY(t)
700 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()700 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
701 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)701 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
702- tb := sess.activeTab()702+ tb := testAttachmentTab(sess)
703 tb.mu.Lock()703 tb.mu.Lock()
704 tb.size = domain.Size{Cols: 100, Rows: 20}704 tb.size = domain.Size{Cols: 100, Rows: 20}
705 tb.mu.Unlock()705 tb.mu.Unlock()
@@ -716,7 +716,7 @@ func TestFloatingMouseIgnoresBorderAndOutside(t *testing.T) {
716 normal := portsmocks.NewMockPTY(t)716 normal := portsmocks.NewMockPTY(t)
717 floatingPTY := portsmocks.NewMockPTY(t)717 floatingPTY := portsmocks.NewMockPTY(t)
718 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)718 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
719- tb := sess.activeTab()719+ tb := testAttachmentTab(sess)
720 tb.mu.Lock()720 tb.mu.Lock()
721 tb.size = domain.Size{Cols: 100, Rows: 20}721 tb.size = domain.Size{Cols: 100, Rows: 20}
722 tb.mu.Unlock()722 tb.mu.Unlock()
@@ -736,7 +736,7 @@ func TestResponsiveDrawerFloatingMouseRoutesOnlyInnerClientCells(t *testing.T) {
736 floatingPTY := portsmocks.NewMockPTY(t)736 floatingPTY := portsmocks.NewMockPTY(t)
737 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()737 floatingPTY.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
738 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)738 d, sess, ac, _ := newManualSessionWithPTYs(t, normal)
739- tb := sess.activeTab()739+ tb := testAttachmentTab(sess)
740 complete := domain.Size{Cols: 79, Rows: 25}740 complete := domain.Size{Cols: 79, Rows: 25}
741 content := tabSize(complete)741 content := tabSize(complete)
742 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())742 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())
@@ -760,7 +760,7 @@ func TestResponsiveDrawerCopyMouseMapsInnerAndPinsDragAwayFromChrome(t *testing.
760 p, release := newBlockingPTY(t)760 p, release := newBlockingPTY(t)
761 defer release()761 defer release()
762 d, sess, ac, _ := newManualSessionWithPTYs(t, p)762 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
763- tb := sess.activeTab()763+ tb := testAttachmentTab(sess)
764 complete := domain.Size{Cols: 79, Rows: 25}764 complete := domain.Size{Cols: 79, Rows: 25}
765 content := tabSize(complete)765 content := tabSize(complete)
766 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())766 geometry := calculateContentFloatingGeometry(content, d.currentFloatingConfig())
@@ -960,12 +960,12 @@ func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) {
960 release()960 release()
961 }961 }
962 }()962 }()
963- sess.active = tc.start963+ selectTestAttachmentTab(sess, tc.start)
964 964
965 d.handleInput(sess, ac, []byte("\x1b "))965 d.handleInput(sess, ac, []byte("\x1b "))
966 d.handleInput(sess, ac, tc.query)966 d.handleInput(sess, ac, tc.query)
967 967
968- require.Equal(t, tc.wantIndex, activeTabIndex(sess))968+ require.Equal(t, tc.wantIndex, testAttachmentTabIndex(sess))
969 })969 })
970 }970 }
971 }971 }
@@ -1200,14 +1200,14 @@ func TestAltXClosesActiveTabAndSelectsRemaining(t *testing.T) {
1200 p2, releasePTY2 := newBlockingPTY(t)1200 p2, releasePTY2 := newBlockingPTY(t)
1201 p3, releasePTY3 := newBlockingPTYWithWrites(t, writes)1201 p3, releasePTY3 := newBlockingPTYWithWrites(t, writes)
1202 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2, p3)1202 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2, p3)
1203- sess.active = 11203+ selectTestAttachmentTab(sess, 1)
1204 1204
1205 d.handleInput(sess, ac, []byte("\x1b "))1205 d.handleInput(sess, ac, []byte("\x1b "))
1206 d.handleInput(sess, ac, []byte("CLT\r"))1206 d.handleInput(sess, ac, []byte("CLT\r"))
1207 1207
1208 require.Equal(t, 1, sessionCount(d))1208 require.Equal(t, 1, sessionCount(d))
1209 require.Len(t, sess.tabs, 2)1209 require.Len(t, sess.tabs, 2)
1210- require.Equal(t, 1, activeTabIndex(sess), "closing middle tab selects the next remaining tab")1210+ require.Equal(t, 1, testAttachmentTabIndex(sess), "closing middle tab selects the next remaining tab")
1211 d.handleInput(sess, ac, []byte("Z"))1211 d.handleInput(sess, ac, []byte("Z"))
1212 require.Eventually(t, func() bool { return len(writes) == 1 }, 2*time.Second, 5*time.Millisecond)1212 require.Eventually(t, func() bool { return len(writes) == 1 }, 2*time.Second, 5*time.Millisecond)
1213 requirePTYWrite(t, writes, []byte("Z"))1213 requirePTYWrite(t, writes, []byte("Z"))
@@ -1258,7 +1258,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) {
1258 release()1258 release()
1259 }1259 }
1260 }()1260 }()
1261- sess.active = 11261+ selectTestAttachmentTab(sess, 1)
1262 1262
1263 d.handleInput(sess, ac, []byte("\x1b "))1263 d.handleInput(sess, ac, []byte("\x1b "))
1264 awaitFrame(t, sends, ports.MsgOutput)1264 awaitFrame(t, sends, ports.MsgOutput)
@@ -1639,7 +1639,7 @@ func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing
1639 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()1639 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()
1640 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1640 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1641 d.procComm = nil1641 d.procComm = nil
1642- tb := sess.activeTab()1642+ tb := testAttachmentTab(sess)
1643 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1643 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1644 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1644 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
1645 tb.mu.Lock()1645 tb.mu.Lock()
@@ -1663,7 +1663,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
1663 {1663 {
1664 name: "split divider",1664 name: "split divider",
1665 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {1665 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {
1666- tb := sess.activeTab()1666+ tb := testAttachmentTab(sess)
1667 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})1667 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
1668 tb.mu.Lock()1668 tb.mu.Lock()
1669 tb.size = domain.Size{Cols: 41, Rows: 10}1669 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -1676,7 +1676,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
1676 {1676 {
1677 name: "split title bar",1677 name: "split title bar",
1678 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {1678 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, _ *[]byte) []byte {
1679- tb := sess.activeTab()1679+ tb := testAttachmentTab(sess)
1680 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})1680 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
1681 tb.mu.Lock()1681 tb.mu.Lock()
1682 tb.size = domain.Size{Cols: 41, Rows: 10}1682 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -1689,7 +1689,7 @@ func TestRejectedLeftReleaseInvalidatesFreshPointerBeforeMotion(t *testing.T) {
1689 {1689 {
1690 name: "floating exterior",1690 name: "floating exterior",
1691 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, press *[]byte) []byte {1691 setup: func(t *testing.T, d *Daemon, sess *session, _ *attachedClient, press *[]byte) []byte {
1692- tb := sess.activeTab()1692+ tb := testAttachmentTab(sess)
1693 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})1693 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
1694 installTestFloating(tb, floating, true)1694 installTestFloating(tb, floating, true)
1695 tb.mu.Lock()1695 tb.mu.Lock()
@@ -1749,7 +1749,7 @@ func TestPressAtFormerStackTitleRowIsTreatedAsExpandedContent(t *testing.T) {
1749 p, release := newBlockingPTY(t)1749 p, release := newBlockingPTY(t)
1750 defer release()1750 defer release()
1751 d, sess, ac, _ := newManualSessionWithPTYs(t, p)1751 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
1752- tb := sess.activeTab()1752+ tb := testAttachmentTab(sess)
1753 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 9})1753 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 9})
1754 p3 := newPane("pane-3", nil, domain.Size{Cols: 20, Rows: 1})1754 p3 := newPane("pane-3", nil, domain.Size{Cols: 20, Rows: 1})
1755 tb.mu.Lock()1755 tb.mu.Lock()
@@ -1821,7 +1821,7 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) {
1821 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()1821 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()
1822 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1822 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1823 d.procComm = nil1823 d.procComm = nil
1824- tb := sess.activeTab()1824+ tb := testAttachmentTab(sess)
1825 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1825 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
1826 tb.mu.Lock()1826 tb.mu.Lock()
1827 tb.size = tc.size1827 tb.size = tc.size
@@ -1844,7 +1844,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) {
1844 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe()1844 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe()
1845 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)1845 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1846 d.procComm = nil1846 d.procComm = nil
1847- tb := sess.activeTab()1847+ tb := testAttachmentTab(sess)
1848 for i := range 10 {1848 for i := range 10 {
1849 copy(tb.focusedPane().screen.Frame.Row(i), testRow(string(rune('a'+i))))1849 copy(tb.focusedPane().screen.Frame.Row(i), testRow(string(rune('a'+i))))
1850 }1850 }
@@ -1878,7 +1878,7 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) {
1878 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()1878 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
1879 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1879 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1880 d.procComm = nil1880 d.procComm = nil
1881- tb := sess.activeTab()1881+ tb := testAttachmentTab(sess)
1882 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1882 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1883 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1883 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
1884 tb.mu.Lock()1884 tb.mu.Lock()
@@ -1905,7 +1905,7 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) {
1905 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe()1905 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe()
1906 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)1906 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1907 d.procComm = nil1907 d.procComm = nil
1908- tb := sess.activeTab()1908+ tb := testAttachmentTab(sess)
1909 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1909 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1910 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1910 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
1911 tb.mu.Lock()1911 tb.mu.Lock()
@@ -1935,7 +1935,7 @@ func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) {
1935 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe()1935 p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe()
1936 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1936 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1937 d.procComm = nil1937 d.procComm = nil
1938- tb := sess.activeTab()1938+ tb := testAttachmentTab(sess)
1939 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})1939 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})
1940 tb.mu.Lock()1940 tb.mu.Lock()
1941 tb.size = domain.Size{Cols: 20, Rows: 5}1941 tb.size = domain.Size{Cols: 20, Rows: 5}
@@ -1954,7 +1954,7 @@ func TestActiveCopyPressOnOtherPaneFocusesAndExitsBeforeFutureInput(t *testing.T
1954 p1, release := newBlockingPTY(t)1954 p1, release := newBlockingPTY(t)
1955 defer release()1955 defer release()
1956 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1956 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1957- tb := sess.activeTab()1957+ tb := testAttachmentTab(sess)
1958 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})1958 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
1959 tb.mu.Lock()1959 tb.mu.Lock()
1960 tb.size = domain.Size{Cols: 41, Rows: 10}1960 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -2053,8 +2053,8 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
2053 name: "press-owned release rejects mapping",2053 name: "press-owned release rejects mapping",
2054 prepare: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) mouse.Event {2054 prepare: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) mouse.Event {
2055 t.Helper()2055 t.Helper()
2056- p := sess.activeTab().focusedPane()2056+ p := testAttachmentTab(sess).focusedPane()
2057- tb := sess.activeTab()2057+ tb := testAttachmentTab(sess)
2058 tb.mu.Lock()2058 tb.mu.Lock()
2059 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 1, 2)2059 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), 1, 2)
2060 tb.mu.Unlock()2060 tb.mu.Unlock()
@@ -2069,7 +2069,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
2069 name: "current geometry disappears",2069 name: "current geometry disappears",
2070 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {2070 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {
2071 t.Helper()2071 t.Helper()
2072- tb := sess.activeTab()2072+ tb := testAttachmentTab(sess)
2073 tb.mu.Lock()2073 tb.mu.Lock()
2074 tb.tree = nil2074 tb.tree = nil
2075 tb.mu.Unlock()2075 tb.mu.Unlock()
@@ -2080,7 +2080,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
2080 name: "fresh press rejects document mapping",2080 name: "fresh press rejects document mapping",
2081 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {2081 prepare: func(t *testing.T, _ *Daemon, sess *session, _ *attachedClient) mouse.Event {
2082 t.Helper()2082 t.Helper()
2083- tb := sess.activeTab()2083+ tb := testAttachmentTab(sess)
2084 tb.mu.Lock()2084 tb.mu.Lock()
2085 tb.size.Rows = 40 // valid layout coordinate, outside the old document.2085 tb.size.Rows = 40 // valid layout coordinate, outside the old document.
2086 tb.mu.Unlock()2086 tb.mu.Unlock()
@@ -2109,7 +2109,7 @@ func TestActiveCopyMouseIgnoresStalePointerResets(t *testing.T) {
2109 }2109 }
2110 defer func() { d.beforeCopyMouseMap = nil }()2110 defer func() { d.beforeCopyMouseMap = nil }()
2111 2111
2112- d.handleActiveCopyMouse(sess, ac, sess.activeTab(), ev)2112+ d.handleActiveCopyMouse(sess, ac, testAttachmentTab(sess), ev)
2113 2113
2114 ac.overlays.copyMu.Lock()2114 ac.overlays.copyMu.Lock()
2115 defer ac.overlays.copyMu.Unlock()2115 defer ac.overlays.copyMu.Unlock()
@@ -2128,7 +2128,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T)
2128 p, release := newBlockingPTY(t)2128 p, release := newBlockingPTY(t)
2129 defer release()2129 defer release()
2130 d, sess, ac, sends := newManualSessionWithPTYs(t, p)2130 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
2131- pane := sess.activeTab().focusedPane()2131+ pane := testAttachmentTab(sess).focusedPane()
2132 for range 8 {2132 for range 8 {
2133 appendHistoryRow(t, pane.history, testRow("history"))2133 appendHistoryRow(t, pane.history, testRow("history"))
2134 }2134 }
@@ -2162,13 +2162,13 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
2162 {2162 {
2163 name: "mono",2163 name: "mono",
2164 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {2164 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {
2165- return sess.activeTab().focusedPane(), mouse.Event{Button: mouse.Left, Type: mouse.Press, Col: 1, Row: 1}2165+ return testAttachmentTab(sess).focusedPane(), mouse.Event{Button: mouse.Left, Type: mouse.Press, Col: 1, Row: 1}
2166 },2166 },
2167 },2167 },
2168 {2168 {
2169 name: "split",2169 name: "split",
2170 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {2170 setup: func(_ *testing.T, _ *Daemon, sess *session) (*pane, mouse.Event) {
2171- tb := sess.activeTab()2171+ tb := testAttachmentTab(sess)
2172 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})2172 p2 := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
2173 tb.mu.Lock()2173 tb.mu.Lock()
2174 tb.size = domain.Size{Cols: 41, Rows: 10}2174 tb.size = domain.Size{Cols: 41, Rows: 10}
@@ -2182,8 +2182,8 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
2182 name: "floating",2182 name: "floating",
2183 setup: func(t *testing.T, d *Daemon, sess *session) (*pane, mouse.Event) {2183 setup: func(t *testing.T, d *Daemon, sess *session) (*pane, mouse.Event) {
2184 p := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})2184 p := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
2185- installTestFloating(sess.activeTab(), p, true)2185+ installTestFloating(testAttachmentTab(sess), p, true)
2186- tb := sess.activeTab()2186+ tb := testAttachmentTab(sess)
2187 tb.mu.Lock()2187 tb.mu.Lock()
2188 _, geometry, visible := tb.visibleFloatingSnapshotLocked(d.currentFloatingConfig())2188 _, geometry, visible := tb.visibleFloatingSnapshotLocked(d.currentFloatingConfig())
2189 tb.mu.Unlock()2189 tb.mu.Unlock()
@@ -2197,7 +2197,7 @@ func TestFreshCopyPressUsesFrameAbsoluteHitTestGeometry(t *testing.T) {
2197 defer release()2197 defer release()
2198 d, sess, ac, _ := newManualSessionWithPTYs(t, p)2198 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
2199 wantPane, ev := tc.setup(t, d, sess)2199 wantPane, ev := tc.setup(t, d, sess)
2200- tb := sess.activeTab()2200+ tb := testAttachmentTab(sess)
2201 tb.mu.Lock()2201 tb.mu.Lock()
2202 wantGeometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), ev.Col, ev.Row)2202 wantGeometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), ev.Col, ev.Row)
2203 tb.mu.Unlock()2203 tb.mu.Unlock()
M internal/usecase/daemon/move.go +0 -2
@@ -78,7 +78,6 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
7878 sourceClient := admission.sourceClient
7979 destinationClient := admission.destinationClient
8080 sourceSnatched := admission.sourceSnatched
81- sourceTabWasActive := admission.sourceTabWasActive
8281 sourceTabInitialGeneration := admission.sourceGeneration
8382 destinationTabInitialGeneration := admission.destinationGeneration
8483 finalSourceTab := admission.finalSourceTab
@@ -215,7 +214,6 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
215214 rolesFrozen: sourceRolesFrozen,
216215 unlockDispatch: unlockDispatch,
217216 reservation: reservation,
218- sourceTabWasActive: sourceTabWasActive,
219217 sourceTabRemoved: commit.sourceTabRemoved,
220218 sourceEmpty: commit.sourceEmpty,
221219 retiredParked: commit.retiredParked,
@@ -78,7 +78,6 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
78 sourceClient := admission.sourceClient78 sourceClient := admission.sourceClient
79 destinationClient := admission.destinationClient79 destinationClient := admission.destinationClient
80 sourceSnatched := admission.sourceSnatched80 sourceSnatched := admission.sourceSnatched
81- sourceTabWasActive := admission.sourceTabWasActive
82 sourceTabInitialGeneration := admission.sourceGeneration81 sourceTabInitialGeneration := admission.sourceGeneration
83 destinationTabInitialGeneration := admission.destinationGeneration82 destinationTabInitialGeneration := admission.destinationGeneration
84 finalSourceTab := admission.finalSourceTab83 finalSourceTab := admission.finalSourceTab
@@ -215,7 +214,6 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
215 rolesFrozen: sourceRolesFrozen,214 rolesFrozen: sourceRolesFrozen,
216 unlockDispatch: unlockDispatch,215 unlockDispatch: unlockDispatch,
217 reservation: reservation,216 reservation: reservation,
218- sourceTabWasActive: sourceTabWasActive,
219 sourceTabRemoved: commit.sourceTabRemoved,217 sourceTabRemoved: commit.sourceTabRemoved,
220 sourceEmpty: commit.sourceEmpty,218 sourceEmpty: commit.sourceEmpty,
221 retiredParked: commit.retiredParked,219 retiredParked: commit.retiredParked,
M internal/usecase/daemon/move_acceptance_test.go +4 -4
@@ -89,7 +89,7 @@ func TestMovePaneAcceptanceRetainsSourceFocusAndDestinationActivity(t *testing.T
8989
9090 destinationActive := newTabWithStableID("destination-active", "active-pane", activePTY, domain.Size{Cols: 80, Rows: 23})
9191 destinationTab := newTabWithStableID("destination-target", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})
92- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{destinationActive, destinationTab}, active: 0}
92+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{destinationActive, destinationTab}}
9393 publishTiledPaneOwners(destination, destinationActive)
9494 publishTiledPaneOwners(destination, destinationTab)
9595 d.mu.Lock()
@@ -140,7 +140,7 @@ func TestMovePaneAcceptanceRetainsSourceFocusAndDestinationActivity(t *testing.T
140140 require.Same(t, remaining, remainingPane)
141141 require.Equal(t, layout.PaneID("pane-2"), sourceFocus, "source focus must use the valid fallback leaf")
142142 destination.mu.Lock()
143- destinationActiveTab := destination.tabs[destination.active]
143+ destinationActiveTab := destination.tabs[testAttachmentTabIndexLocked(destination)]
144144 destination.mu.Unlock()
145145 require.Same(t, destinationActive, destinationActiveTab, "destination active tab must remain unchanged")
146146 destinationTab.mu.Lock()
@@ -182,7 +182,7 @@ func TestMovePaneRejectsStaleIncarnationWithoutMutation(t *testing.T) {
182182 sourceTab := source.tabs[0]
183183 sourceTab.stableID = "source-tab"
184184 moved := sourceTab.focusedPane()
185- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}
185+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
186186 destinationTab := destination.tabs[0]
187187 publishTiledPaneOwners(destination, destinationTab)
188188 d.mu.Lock()
@@ -231,7 +231,7 @@ func TestMovePaneRejectsStaleLayoutGenerationBeforeMutation(t *testing.T) {
231231 sourceTab := source.tabs[0]
232232 sourceTab.stableID = "source-tab"
233233 moved := sourceTab.focusedPane()
234- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}
234+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
235235 destinationTab := destination.tabs[0]
236236 publishTiledPaneOwners(destination, destinationTab)
237237 d.mu.Lock()
@@ -89,7 +89,7 @@ func TestMovePaneAcceptanceRetainsSourceFocusAndDestinationActivity(t *testing.T
89 89
90 destinationActive := newTabWithStableID("destination-active", "active-pane", activePTY, domain.Size{Cols: 80, Rows: 23})90 destinationActive := newTabWithStableID("destination-active", "active-pane", activePTY, domain.Size{Cols: 80, Rows: 23})
91 destinationTab := newTabWithStableID("destination-target", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})91 destinationTab := newTabWithStableID("destination-target", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})
92- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{destinationActive, destinationTab}, active: 0}92+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{destinationActive, destinationTab}}
93 publishTiledPaneOwners(destination, destinationActive)93 publishTiledPaneOwners(destination, destinationActive)
94 publishTiledPaneOwners(destination, destinationTab)94 publishTiledPaneOwners(destination, destinationTab)
95 d.mu.Lock()95 d.mu.Lock()
@@ -140,7 +140,7 @@ func TestMovePaneAcceptanceRetainsSourceFocusAndDestinationActivity(t *testing.T
140 require.Same(t, remaining, remainingPane)140 require.Same(t, remaining, remainingPane)
141 require.Equal(t, layout.PaneID("pane-2"), sourceFocus, "source focus must use the valid fallback leaf")141 require.Equal(t, layout.PaneID("pane-2"), sourceFocus, "source focus must use the valid fallback leaf")
142 destination.mu.Lock()142 destination.mu.Lock()
143- destinationActiveTab := destination.tabs[destination.active]143+ destinationActiveTab := destination.tabs[testAttachmentTabIndexLocked(destination)]
144 destination.mu.Unlock()144 destination.mu.Unlock()
145 require.Same(t, destinationActive, destinationActiveTab, "destination active tab must remain unchanged")145 require.Same(t, destinationActive, destinationActiveTab, "destination active tab must remain unchanged")
146 destinationTab.mu.Lock()146 destinationTab.mu.Lock()
@@ -182,7 +182,7 @@ func TestMovePaneRejectsStaleIncarnationWithoutMutation(t *testing.T) {
182 sourceTab := source.tabs[0]182 sourceTab := source.tabs[0]
183 sourceTab.stableID = "source-tab"183 sourceTab.stableID = "source-tab"
184 moved := sourceTab.focusedPane()184 moved := sourceTab.focusedPane()
185- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}185+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
186 destinationTab := destination.tabs[0]186 destinationTab := destination.tabs[0]
187 publishTiledPaneOwners(destination, destinationTab)187 publishTiledPaneOwners(destination, destinationTab)
188 d.mu.Lock()188 d.mu.Lock()
@@ -231,7 +231,7 @@ func TestMovePaneRejectsStaleLayoutGenerationBeforeMutation(t *testing.T) {
231 sourceTab := source.tabs[0]231 sourceTab := source.tabs[0]
232 sourceTab.stableID = "source-tab"232 sourceTab.stableID = "source-tab"
233 moved := sourceTab.focusedPane()233 moved := sourceTab.focusedPane()
234- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}234+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
235 destinationTab := destination.tabs[0]235 destinationTab := destination.tabs[0]
236 publishTiledPaneOwners(destination, destinationTab)236 publishTiledPaneOwners(destination, destinationTab)
237 d.mu.Lock()237 d.mu.Lock()
M internal/usecase/daemon/move_atomicity_test.go +2 -3
@@ -67,10 +67,10 @@ func readMoveCommitObservation(d *Daemon, source, destination *session, sourceTa
6767 }
6868
6969 func activeMoveTabLocked(sess *session) *tab {
70- if sess == nil || sess.active < 0 || sess.active >= len(sess.tabs) {
70+ if sess == nil || testAttachmentTabIndexLocked(sess) < 0 || testAttachmentTabIndexLocked(sess) >= len(sess.tabs) {
7171 return nil
7272 }
73- return sess.tabs[sess.active]
73+ return sess.tabs[testAttachmentTabIndexLocked(sess)]
7474 }
7575
7676 func TestMovePaneCommitPointHidesPartialPublication(t *testing.T) {
@@ -87,7 +87,6 @@ func TestMovePaneCommitPointHidesPartialPublication(t *testing.T) {
8787 destination := &session{sessionCore: sessionCore{id: "destination",
8888 name: "destination",
8989 ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", p2, domain.Size{Cols: 80, Rows: 23})},
90- active: 0,
9190 }
9291 destinationTab := destination.tabs[0]
9392 publishTiledPaneOwners(destination, destinationTab)
@@ -67,10 +67,10 @@ func readMoveCommitObservation(d *Daemon, source, destination *session, sourceTa
67 }67 }
68 68
69 func activeMoveTabLocked(sess *session) *tab {69 func activeMoveTabLocked(sess *session) *tab {
70- if sess == nil || sess.active < 0 || sess.active >= len(sess.tabs) {70+ if sess == nil || testAttachmentTabIndexLocked(sess) < 0 || testAttachmentTabIndexLocked(sess) >= len(sess.tabs) {
71 return nil71 return nil
72 }72 }
73- return sess.tabs[sess.active]73+ return sess.tabs[testAttachmentTabIndexLocked(sess)]
74 }74 }
75 75
76 func TestMovePaneCommitPointHidesPartialPublication(t *testing.T) {76 func TestMovePaneCommitPointHidesPartialPublication(t *testing.T) {
@@ -87,7 +87,6 @@ func TestMovePaneCommitPointHidesPartialPublication(t *testing.T) {
87 destination := &session{sessionCore: sessionCore{id: "destination",87 destination := &session{sessionCore: sessionCore{id: "destination",
88 name: "destination",88 name: "destination",
89 ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", p2, domain.Size{Cols: 80, Rows: 23})},89 ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", p2, domain.Size{Cols: 80, Rows: 23})},
90- active: 0,
91 }90 }
92 destinationTab := destination.tabs[0]91 destinationTab := destination.tabs[0]
93 publishTiledPaneOwners(destination, destinationTab)92 publishTiledPaneOwners(destination, destinationTab)
M internal/usecase/daemon/move_cleanup_test.go +2 -6
@@ -26,9 +26,7 @@ func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) {
2626 client.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}
2727 client.sendMu.Unlock()
2828
29- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},
30- active: 0,
31- }
29+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
3230 destinationTab := destination.tabs[0]
3331 publishTiledPaneOwners(destination, destinationTab)
3432 d.mu.Lock()
@@ -104,9 +102,7 @@ func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) {
104102 moved := sourceTab.focusedPane()
105103 require.NotNil(t, d.attachCoordinator(source, nil, displaced, true))
106104
107- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},
108- active: 0,
109- }
105+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
110106 destinationTab := destination.tabs[0]
111107 publishTiledPaneOwners(destination, destinationTab)
112108 d.mu.Lock()
@@ -26,9 +26,7 @@ func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) {
26 client.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}26 client.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}
27 client.sendMu.Unlock()27 client.sendMu.Unlock()
28 28
29- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},29+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
30- active: 0,
31- }
32 destinationTab := destination.tabs[0]30 destinationTab := destination.tabs[0]
33 publishTiledPaneOwners(destination, destinationTab)31 publishTiledPaneOwners(destination, destinationTab)
34 d.mu.Lock()32 d.mu.Lock()
@@ -104,9 +102,7 @@ func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) {
104 moved := sourceTab.focusedPane()102 moved := sourceTab.focusedPane()
105 require.NotNil(t, d.attachCoordinator(source, nil, displaced, true))103 require.NotNil(t, d.attachCoordinator(source, nil, displaced, true))
106 104
107- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},105+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
108- active: 0,
109- }
110 destinationTab := destination.tabs[0]106 destinationTab := destination.tabs[0]
111 publishTiledPaneOwners(destination, destinationTab)107 publishTiledPaneOwners(destination, destinationTab)
112 d.mu.Lock()108 d.mu.Lock()
M internal/usecase/daemon/move_gate_race_test.go +1 -3
@@ -21,9 +21,7 @@ func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *sess
2121 moved := sourceTab.focusedPane()
2222 require.NotNil(t, d.attachCoordinator(source, nil, client, true))
2323
24- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},
25- active: 0,
26- }
24+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
2725 destinationTab := destination.tabs[0]
2826 publishTiledPaneOwners(destination, destinationTab)
2927 d.mu.Lock()
@@ -21,9 +21,7 @@ func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *sess
21 moved := sourceTab.focusedPane()21 moved := sourceTab.focusedPane()
22 require.NotNil(t, d.attachCoordinator(source, nil, client, true))22 require.NotNil(t, d.attachCoordinator(source, nil, client, true))
23 23
24- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},24+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
25- active: 0,
26- }
27 destinationTab := destination.tabs[0]25 destinationTab := destination.tabs[0]
28 publishTiledPaneOwners(destination, destinationTab)26 publishTiledPaneOwners(destination, destinationTab)
29 d.mu.Lock()27 d.mu.Lock()
M internal/usecase/daemon/move_pane_admission.go +1 -11
@@ -1,9 +1,6 @@
11 package daemon
22
3-import (
4- "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/usecase/layout"
6-)
3+import "github.com/bnema/vev/internal/usecase/layout"
74
85 // movePaneAdmission is an immutable snapshot of the exact live objects and
96 // generations a move may later commit. Resize fences are acquired only after
@@ -20,7 +17,6 @@ type movePaneAdmission struct {
2017 sourceGeneration uint64
2118 destinationGeneration uint64
2219 finalSourceTab bool
23- sourceTabWasActive bool
2420 }
2521
2622 func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destination *session) (*movePaneAdmission, error) {
@@ -57,11 +53,6 @@ func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destinat
5753 return nil, errMoveStaleTarget
5854 }
5955
60- sourceTabWasActive := false
61- if req.Client != nil {
62- view := req.Client.viewSnapshot()
63- sourceTabWasActive = view.tabID == domain.TabStableID(sourceTab.stableID)
64- }
6556 return &movePaneAdmission{
6657 source: source,
6758 destination: destination,
@@ -75,6 +66,5 @@ func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destinat
7566 destinationGeneration: destinationTab.layoutGeneration,
7667 finalSourceTab: len(source.tabs) == 1 && source.tabs[0] == sourceTab &&
7768 sourceTab.tree != nil && sourceTab.tree.Root != nil && len(layout.LeafIDs(sourceTab.tree.Root)) == 1,
78- sourceTabWasActive: sourceTabWasActive,
7969 }, nil
8070 }
@@ -1,9 +1,6 @@
1 package daemon1 package daemon
2 2
3-import (3+import "github.com/bnema/vev/internal/usecase/layout"
4- "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/usecase/layout"
6-)
7 4
8 // movePaneAdmission is an immutable snapshot of the exact live objects and5 // movePaneAdmission is an immutable snapshot of the exact live objects and
9 // generations a move may later commit. Resize fences are acquired only after6 // generations a move may later commit. Resize fences are acquired only after
@@ -20,7 +17,6 @@ type movePaneAdmission struct {
20 sourceGeneration uint6417 sourceGeneration uint64
21 destinationGeneration uint6418 destinationGeneration uint64
22 finalSourceTab bool19 finalSourceTab bool
23- sourceTabWasActive bool
24 }20 }
25 21
26 func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destination *session) (*movePaneAdmission, error) {22 func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destination *session) (*movePaneAdmission, error) {
@@ -57,11 +53,6 @@ func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destinat
57 return nil, errMoveStaleTarget53 return nil, errMoveStaleTarget
58 }54 }
59 55
60- sourceTabWasActive := false
61- if req.Client != nil {
62- view := req.Client.viewSnapshot()
63- sourceTabWasActive = view.tabID == domain.TabStableID(sourceTab.stableID)
64- }
65 return &movePaneAdmission{56 return &movePaneAdmission{
66 source: source,57 source: source,
67 destination: destination,58 destination: destination,
@@ -75,6 +66,5 @@ func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destinat
75 destinationGeneration: destinationTab.layoutGeneration,66 destinationGeneration: destinationTab.layoutGeneration,
76 finalSourceTab: len(source.tabs) == 1 && source.tabs[0] == sourceTab &&67 finalSourceTab: len(source.tabs) == 1 && source.tabs[0] == sourceTab &&
77 sourceTab.tree != nil && sourceTab.tree.Root != nil && len(layout.LeafIDs(sourceTab.tree.Root)) == 1,68 sourceTab.tree != nil && sourceTab.tree.Root != nil && len(layout.LeafIDs(sourceTab.tree.Root)) == 1,
78- sourceTabWasActive: sourceTabWasActive,
79 }, nil69 }, nil
80 }70 }
M internal/usecase/daemon/move_pane_postcommit.go +3 -4
@@ -36,9 +36,8 @@ type movePanePostcommitPlan struct {
3636 reservation *moveLifecycleReservation
3737 oldTabCancel context.CancelFunc
3838
39- sourceTabWasActive bool
40- sourceTabRemoved bool
41- sourceEmpty bool
39+ sourceTabRemoved bool
40+ sourceEmpty bool
4241
4342 retiredParked []parkedAttachmentRetirement
4443 retiredAttachments []detachedAttachmentSnapshot
@@ -94,7 +93,7 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
9493 if !moveSessionRetired(d, p.source) {
9594 sourceLayoutTab := p.sourceTab
9695 if p.sourceTabRemoved {
97- sourceLayoutTab = p.source.tabForAttachmentOrActive(nil)
96+ sourceLayoutTab = p.source.firstTab()
9897 }
9998 if sourceLayoutTab != nil && sourceLayoutTab != p.destinationTab {
10099 d.applyTabLayout(p.source, sourceLayoutTab)
@@ -36,9 +36,8 @@ type movePanePostcommitPlan struct {
36 reservation *moveLifecycleReservation36 reservation *moveLifecycleReservation
37 oldTabCancel context.CancelFunc37 oldTabCancel context.CancelFunc
38 38
39- sourceTabWasActive bool39+ sourceTabRemoved bool
40- sourceTabRemoved bool40+ sourceEmpty bool
41- sourceEmpty bool
42 41
43 retiredParked []parkedAttachmentRetirement42 retiredParked []parkedAttachmentRetirement
44 retiredAttachments []detachedAttachmentSnapshot43 retiredAttachments []detachedAttachmentSnapshot
@@ -94,7 +93,7 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
94 if !moveSessionRetired(d, p.source) {93 if !moveSessionRetired(d, p.source) {
95 sourceLayoutTab := p.sourceTab94 sourceLayoutTab := p.sourceTab
96 if p.sourceTabRemoved {95 if p.sourceTabRemoved {
97- sourceLayoutTab = p.source.tabForAttachmentOrActive(nil)96+ sourceLayoutTab = p.source.firstTab()
98 }97 }
99 if sourceLayoutTab != nil && sourceLayoutTab != p.destinationTab {98 if sourceLayoutTab != nil && sourceLayoutTab != p.destinationTab {
100 d.applyTabLayout(p.source, sourceLayoutTab)99 d.applyTabLayout(p.source, sourceLayoutTab)
M internal/usecase/daemon/move_picker.go +2 -2
@@ -60,7 +60,7 @@ func (d *Daemon) enterPickerForIntent(sess *session, ac *attachedClient, intent
6060 if intent != pickerNavigate && !sess.capabilities().yieldsMoves() {
6161 return errSessionCannotYieldMoves
6262 }
63- model := d.newPickerModel(sess, intent, source, picker.SourceFilter{})
63+ model := d.newPickerModel(sess, ac, intent, source, picker.SourceFilter{})
6464 if intent != pickerNavigate {
6565 if _, ok := model.Selected(); !ok {
6666 return errNoMoveDestination
@@ -172,7 +172,7 @@ func (d *Daemon) refreshPickerOpts(ac *attachedClient, opts pickerRefreshOptions
172172 }
173173 }
174174 rt.pickerMu.Unlock()
175- model := d.newPickerModel(sess, intent, source, current)
175+ model := d.newPickerModel(sess, ac, intent, source, current)
176176 if rt.afterPickerRefreshBuild != nil {
177177 rt.afterPickerRefreshBuild(model)
178178 }
@@ -60,7 +60,7 @@ func (d *Daemon) enterPickerForIntent(sess *session, ac *attachedClient, intent
60 if intent != pickerNavigate && !sess.capabilities().yieldsMoves() {60 if intent != pickerNavigate && !sess.capabilities().yieldsMoves() {
61 return errSessionCannotYieldMoves61 return errSessionCannotYieldMoves
62 }62 }
63- model := d.newPickerModel(sess, intent, source, picker.SourceFilter{})63+ model := d.newPickerModel(sess, ac, intent, source, picker.SourceFilter{})
64 if intent != pickerNavigate {64 if intent != pickerNavigate {
65 if _, ok := model.Selected(); !ok {65 if _, ok := model.Selected(); !ok {
66 return errNoMoveDestination66 return errNoMoveDestination
@@ -172,7 +172,7 @@ func (d *Daemon) refreshPickerOpts(ac *attachedClient, opts pickerRefreshOptions
172 }172 }
173 }173 }
174 rt.pickerMu.Unlock()174 rt.pickerMu.Unlock()
175- model := d.newPickerModel(sess, intent, source, current)175+ model := d.newPickerModel(sess, ac, intent, source, current)
176 if rt.afterPickerRefreshBuild != nil {176 if rt.afterPickerRefreshBuild != nil {
177 rt.afterPickerRefreshBuild(model)177 rt.afterPickerRefreshBuild(model)
178 }178 }
M internal/usecase/daemon/move_picker_test.go +2 -2
@@ -35,7 +35,7 @@ func setupMovePickerSessionsWithClock(t *testing.T, clock ports.Clock, extraDest
3535 releases = append(releases, releaseDest)
3636 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destPTY, domain.Size{Cols: 80, Rows: 23})
3737 publishTiledPaneOwners(source, sourceTab)
38- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{destinationTab}, active: 0}
38+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{destinationTab}}
3939 publishTiledPaneOwners(destination, destinationTab)
4040 for range extraDestinationTabs {
4141 extraPTY, releaseExtra := newBlockingPTY(t)
@@ -306,7 +306,7 @@ func TestMovePickerRefreshCloseKeepsReplacementPicker(t *testing.T) {
306306 t.Fatal("timed out waiting for move-picker refresh rebuild")
307307 }
308308
309- replacement := d.newPickerModel(source, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
309+ replacement := d.newPickerModel(source, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
310310 d.publishPicker(source, ac, replacement, pickerNavigate, moveSourceLocator{})
311311 close(allowClose)
312312 select {
@@ -35,7 +35,7 @@ func setupMovePickerSessionsWithClock(t *testing.T, clock ports.Clock, extraDest
35 releases = append(releases, releaseDest)35 releases = append(releases, releaseDest)
36 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destPTY, domain.Size{Cols: 80, Rows: 23})36 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destPTY, domain.Size{Cols: 80, Rows: 23})
37 publishTiledPaneOwners(source, sourceTab)37 publishTiledPaneOwners(source, sourceTab)
38- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{destinationTab}, active: 0}38+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{destinationTab}}
39 publishTiledPaneOwners(destination, destinationTab)39 publishTiledPaneOwners(destination, destinationTab)
40 for range extraDestinationTabs {40 for range extraDestinationTabs {
41 extraPTY, releaseExtra := newBlockingPTY(t)41 extraPTY, releaseExtra := newBlockingPTY(t)
@@ -306,7 +306,7 @@ func TestMovePickerRefreshCloseKeepsReplacementPicker(t *testing.T) {
306 t.Fatal("timed out waiting for move-picker refresh rebuild")306 t.Fatal("timed out waiting for move-picker refresh rebuild")
307 }307 }
308 308
309- replacement := d.newPickerModel(source, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})309+ replacement := d.newPickerModel(source, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
310 d.publishPicker(source, ac, replacement, pickerNavigate, moveSourceLocator{})310 d.publishPicker(source, ac, replacement, pickerNavigate, moveSourceLocator{})
311 close(allowClose)311 close(allowClose)
312 select {312 select {
M internal/usecase/daemon/move_stale_destination_test.go +1 -1
@@ -18,7 +18,7 @@ func TestMovePaneRejectsStaleDestinationIncarnationWithoutMutation(t *testing.T)
1818 sourceTab := source.tabs[0]
1919 sourceTab.stableID = "source-tab"
2020 moved := sourceTab.focusedPane()
21- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}
21+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
2222 destinationTab := destination.tabs[0]
2323 publishTiledPaneOwners(destination, destinationTab)
2424 d.mu.Lock()
@@ -18,7 +18,7 @@ func TestMovePaneRejectsStaleDestinationIncarnationWithoutMutation(t *testing.T)
18 sourceTab := source.tabs[0]18 sourceTab := source.tabs[0]
19 sourceTab.stableID = "source-tab"19 sourceTab.stableID = "source-tab"
20 moved := sourceTab.focusedPane()20 moved := sourceTab.focusedPane()
21- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}, active: 0}21+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
22 destinationTab := destination.tabs[0]22 destinationTab := destination.tabs[0]
23 publishTiledPaneOwners(destination, destinationTab)23 publishTiledPaneOwners(destination, destinationTab)
24 d.mu.Lock()24 d.mu.Lock()
M internal/usecase/daemon/move_tab.go +0 -1
@@ -185,7 +185,6 @@ func (d *Daemon) moveTab(req moveTabRequest) (result error) {
185185 unlockDispatch: unlockDispatch,
186186 reservation: reservation,
187187 oldTabCancel: commit.oldTabCancel,
188- sourceTabWasActive: !commit.sourceEmpty,
189188 sourceTabRemoved: true,
190189 sourceEmpty: commit.sourceEmpty,
191190 retiredParked: commit.retiredParked,
@@ -185,7 +185,6 @@ func (d *Daemon) moveTab(req moveTabRequest) (result error) {
185 unlockDispatch: unlockDispatch,185 unlockDispatch: unlockDispatch,
186 reservation: reservation,186 reservation: reservation,
187 oldTabCancel: commit.oldTabCancel,187 oldTabCancel: commit.oldTabCancel,
188- sourceTabWasActive: !commit.sourceEmpty,
189 sourceTabRemoved: true,188 sourceTabRemoved: true,
190 sourceEmpty: commit.sourceEmpty,189 sourceEmpty: commit.sourceEmpty,
191 retiredParked: commit.retiredParked,190 retiredParked: commit.retiredParked,
M internal/usecase/daemon/move_tab_test.go +9 -9
@@ -27,13 +27,13 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
2727 moved.name = "preserved-name"
2828 remaining := source.tabs[1]
2929 remaining.stableID = "remaining-tab"
30- source.active = 0
30+ selectTestAttachmentTab(source, 0)
3131
3232 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)
3333 t.Cleanup(destinationCancel)
3434 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 100, Rows: 30})
3535 destinationTab.ctx, destinationTab.cancel = context.WithCancel(destinationCtx)
36- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{9}, ephemeral: destinationEphemeral}, ctx: destinationCtx, cancel: destinationCancel, tabs: []*tab{destinationTab}, active: 0}
36+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{9}, ephemeral: destinationEphemeral}, ctx: destinationCtx, cancel: destinationCancel, tabs: []*tab{destinationTab}}
3737 publishTiledPaneOwners(destination, destinationTab)
3838
3939 floating := newPaneWithStableID("floating", "floating-stable", floatingPTY, domain.Size{Cols: 20, Rows: 8})
@@ -53,7 +53,7 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
5353 oldWorkerCtx := moved.ctx
5454 oldTree := moved.tree
5555 oldPanes := moved.panes
56- beforeDestinationActive := destination.tabs[destination.active]
56+ beforeDestinationActive := destination.tabs[testAttachmentTabIndex(destination)]
5757
5858 err := d.moveTab(moveTabRequest{
5959 Source: moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
@@ -63,10 +63,10 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
6363 require.NoError(t, err)
6464
6565 require.Equal(t, []*tab{remaining}, source.tabs)
66- require.Zero(t, source.active)
67- require.Same(t, remaining, source.tabs[source.active])
66+ require.Zero(t, testAttachmentTabIndex(source))
67+ require.Same(t, remaining, source.tabs[testAttachmentTabIndex(source)])
6868 require.Equal(t, []*tab{destinationTab, moved}, destination.tabs)
69- require.Same(t, beforeDestinationActive, destination.tabs[destination.active], "moved tab stays in the background")
69+ require.Same(t, beforeDestinationActive, destination.tabs[testAttachmentTabIndex(destination)], "moved tab stays in the background")
7070 require.Equal(t, destinationTab.size, moved.size, "destination geometry becomes authoritative")
7171 require.Equal(t, "preserved-name", moved.name)
7272 require.Same(t, oldTree, moved.tree)
@@ -115,7 +115,7 @@ func TestMoveTabFinalSourceVisibleFloatingPreservesRuntimeAndFollowsClient(t *te
115115 Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
116116 }))
117117
118- require.Same(t, moved, destination.tabs[destination.active])
118+ require.Same(t, moved, destination.tabs[testAttachmentTabIndex(destination)])
119119 require.Contains(t, destination.snapshotAttachments(), client)
120120 require.Same(t, destination, client.currentSession())
121121 moved.mu.Lock()
@@ -162,7 +162,7 @@ func TestMoveTabFinalSourceClientFollowsAndActivatesMovedTab(t *testing.T) {
162162 require.Contains(t, destination.snapshotAttachments(), client)
163163 require.Equal(t, attachmentActive, destination.attachmentRole(client))
164164 require.Equal(t, []*tab{oldDestinationActive, moved}, destination.tabs)
165- require.Same(t, moved, destination.tabs[destination.active], "final-source follower activates the moved tab")
165+ require.Same(t, moved, destination.tabs[testAttachmentTabIndex(destination)], "final-source follower activates the moved tab")
166166 require.True(t, rebased)
167167 }
168168
@@ -572,7 +572,7 @@ func addMoveTabTestSession(d *Daemon, id domain.SessionID, tabID string) *sessio
572572 ctx, cancel := context.WithCancel(d.serveCtx)
573573 tb := newTabWithStableID(tabID, tabID+"-pane", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
574574 tb.ctx, tb.cancel = context.WithCancel(ctx)
575- sess := &session{sessionCore: sessionCore{id: id, name: string(id), incarnation: domain.IncarnationID{5}, ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}, active: 0}
575+ sess := &session{sessionCore: sessionCore{id: id, name: string(id), incarnation: domain.IncarnationID{5}, ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}}
576576 publishTiledPaneOwners(sess, tb)
577577 d.mu.Lock()
578578 d.sessions[sess.id] = sess
@@ -27,13 +27,13 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
27 moved.name = "preserved-name"27 moved.name = "preserved-name"
28 remaining := source.tabs[1]28 remaining := source.tabs[1]
29 remaining.stableID = "remaining-tab"29 remaining.stableID = "remaining-tab"
30- source.active = 030+ selectTestAttachmentTab(source, 0)
31 31
32 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)32 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)
33 t.Cleanup(destinationCancel)33 t.Cleanup(destinationCancel)
34 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 100, Rows: 30})34 destinationTab := newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 100, Rows: 30})
35 destinationTab.ctx, destinationTab.cancel = context.WithCancel(destinationCtx)35 destinationTab.ctx, destinationTab.cancel = context.WithCancel(destinationCtx)
36- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{9}, ephemeral: destinationEphemeral}, ctx: destinationCtx, cancel: destinationCancel, tabs: []*tab{destinationTab}, active: 0}36+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{9}, ephemeral: destinationEphemeral}, ctx: destinationCtx, cancel: destinationCancel, tabs: []*tab{destinationTab}}
37 publishTiledPaneOwners(destination, destinationTab)37 publishTiledPaneOwners(destination, destinationTab)
38 38
39 floating := newPaneWithStableID("floating", "floating-stable", floatingPTY, domain.Size{Cols: 20, Rows: 8})39 floating := newPaneWithStableID("floating", "floating-stable", floatingPTY, domain.Size{Cols: 20, Rows: 8})
@@ -53,7 +53,7 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
53 oldWorkerCtx := moved.ctx53 oldWorkerCtx := moved.ctx
54 oldTree := moved.tree54 oldTree := moved.tree
55 oldPanes := moved.panes55 oldPanes := moved.panes
56- beforeDestinationActive := destination.tabs[destination.active]56+ beforeDestinationActive := destination.tabs[testAttachmentTabIndex(destination)]
57 57
58 err := d.moveTab(moveTabRequest{58 err := d.moveTab(moveTabRequest{
59 Source: moveSessionLocator{ID: source.id, Incarnation: source.incarnation},59 Source: moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
@@ -63,10 +63,10 @@ func TestMoveTabPreservesWholeTabAndActivatesLogicalNeighbor(t *testing.T) {
63 require.NoError(t, err)63 require.NoError(t, err)
64 64
65 require.Equal(t, []*tab{remaining}, source.tabs)65 require.Equal(t, []*tab{remaining}, source.tabs)
66- require.Zero(t, source.active)66+ require.Zero(t, testAttachmentTabIndex(source))
67- require.Same(t, remaining, source.tabs[source.active])67+ require.Same(t, remaining, source.tabs[testAttachmentTabIndex(source)])
68 require.Equal(t, []*tab{destinationTab, moved}, destination.tabs)68 require.Equal(t, []*tab{destinationTab, moved}, destination.tabs)
69- require.Same(t, beforeDestinationActive, destination.tabs[destination.active], "moved tab stays in the background")69+ require.Same(t, beforeDestinationActive, destination.tabs[testAttachmentTabIndex(destination)], "moved tab stays in the background")
70 require.Equal(t, destinationTab.size, moved.size, "destination geometry becomes authoritative")70 require.Equal(t, destinationTab.size, moved.size, "destination geometry becomes authoritative")
71 require.Equal(t, "preserved-name", moved.name)71 require.Equal(t, "preserved-name", moved.name)
72 require.Same(t, oldTree, moved.tree)72 require.Same(t, oldTree, moved.tree)
@@ -115,7 +115,7 @@ func TestMoveTabFinalSourceVisibleFloatingPreservesRuntimeAndFollowsClient(t *te
115 Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},115 Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
116 }))116 }))
117 117
118- require.Same(t, moved, destination.tabs[destination.active])118+ require.Same(t, moved, destination.tabs[testAttachmentTabIndex(destination)])
119 require.Contains(t, destination.snapshotAttachments(), client)119 require.Contains(t, destination.snapshotAttachments(), client)
120 require.Same(t, destination, client.currentSession())120 require.Same(t, destination, client.currentSession())
121 moved.mu.Lock()121 moved.mu.Lock()
@@ -162,7 +162,7 @@ func TestMoveTabFinalSourceClientFollowsAndActivatesMovedTab(t *testing.T) {
162 require.Contains(t, destination.snapshotAttachments(), client)162 require.Contains(t, destination.snapshotAttachments(), client)
163 require.Equal(t, attachmentActive, destination.attachmentRole(client))163 require.Equal(t, attachmentActive, destination.attachmentRole(client))
164 require.Equal(t, []*tab{oldDestinationActive, moved}, destination.tabs)164 require.Equal(t, []*tab{oldDestinationActive, moved}, destination.tabs)
165- require.Same(t, moved, destination.tabs[destination.active], "final-source follower activates the moved tab")165+ require.Same(t, moved, destination.tabs[testAttachmentTabIndex(destination)], "final-source follower activates the moved tab")
166 require.True(t, rebased)166 require.True(t, rebased)
167 }167 }
168 168
@@ -572,7 +572,7 @@ func addMoveTabTestSession(d *Daemon, id domain.SessionID, tabID string) *sessio
572 ctx, cancel := context.WithCancel(d.serveCtx)572 ctx, cancel := context.WithCancel(d.serveCtx)
573 tb := newTabWithStableID(tabID, tabID+"-pane", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})573 tb := newTabWithStableID(tabID, tabID+"-pane", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
574 tb.ctx, tb.cancel = context.WithCancel(ctx)574 tb.ctx, tb.cancel = context.WithCancel(ctx)
575- sess := &session{sessionCore: sessionCore{id: id, name: string(id), incarnation: domain.IncarnationID{5}, ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}, active: 0}575+ sess := &session{sessionCore: sessionCore{id: id, name: string(id), incarnation: domain.IncarnationID{5}, ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{tb}}
576 publishTiledPaneOwners(sess, tb)576 publishTiledPaneOwners(sess, tb)
577 d.mu.Lock()577 d.mu.Lock()
578 d.sessions[sess.id] = sess578 d.sessions[sess.id] = sess
M internal/usecase/daemon/move_test.go +5 -9
@@ -57,7 +57,7 @@ func TestMovePaneMutationCrossSession(t *testing.T) {
5757 movedCtx, movedCancel := context.WithCancel(d.paneProcessCtx)
5858 moved.ctx, moved.cancel = movedCtx, movedCancel
5959
60- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}, active: 0}
60+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}}
6161 destinationTab := destination.tabs[0]
6262 destinationTab.stableID = "destination-tab"
6363 publishTiledPaneOwners(destination, destinationTab)
@@ -97,7 +97,7 @@ func TestMovePaneFinalSourceClientFollowsDestination(t *testing.T) {
9797 require.NotNil(t, rc)
9898 var rebased atomic.Bool
9999 client.renderStages.handoffRebase = func() { rebased.Store(true) }
100- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}, active: 0}
100+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}}
101101 destinationTab := destination.tabs[0]
102102 destinationTab.stableID = "destination-tab"
103103 publishTiledPaneOwners(destination, destinationTab)
@@ -117,7 +117,7 @@ func TestMovePaneFinalSourceClientFollowsDestination(t *testing.T) {
117117 require.Same(t, destination, client.currentSession())
118118 require.Contains(t, destination.snapshotAttachments(), client)
119119 require.Equal(t, attachmentActive, destination.attachmentRole(client))
120- require.Same(t, destinationTab, destination.tabs[destination.active])
120+ require.Same(t, destinationTab, destination.tabs[testAttachmentTabIndex(destination)])
121121 require.True(t, rebased.Load(), "final-source follower must rebase output before its first paint")
122122 require.NoError(t, moved.ctx.Err(), "transferred pane process must remain alive")
123123 }
@@ -140,9 +140,7 @@ func TestMovePaneRetiresEmptySourceWithoutClosingTransferredResources(t *testing
140140 movedTab.stableID = "source-tab"
141141
142142 destinationPTY := newQuietPTY()
143- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},
144- active: 0,
145- }
143+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
146144 publishTiledPaneOwners(destination, destination.tabs[0])
147145 d.mu.Lock()
148146 d.sessions[destination.id] = destination
@@ -190,9 +188,7 @@ func TestMovePaneRetiresSourceParkedClients(t *testing.T) {
190188 parked.initOverlays()
191189 require.True(t, d.parkAttachment(source, parked))
192190 destinationPTY := newQuietPTY()
193- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},
194- active: 0,
195- }
191+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
196192 publishTiledPaneOwners(destination, destination.tabs[0])
197193 d.mu.Lock()
198194 d.sessions[destination.id] = destination
@@ -57,7 +57,7 @@ func TestMovePaneMutationCrossSession(t *testing.T) {
57 movedCtx, movedCancel := context.WithCancel(d.paneProcessCtx)57 movedCtx, movedCancel := context.WithCancel(d.paneProcessCtx)
58 moved.ctx, moved.cancel = movedCtx, movedCancel58 moved.ctx, moved.cancel = movedCtx, movedCancel
59 59
60- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}, active: 0}60+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}}
61 destinationTab := destination.tabs[0]61 destinationTab := destination.tabs[0]
62 destinationTab.stableID = "destination-tab"62 destinationTab.stableID = "destination-tab"
63 publishTiledPaneOwners(destination, destinationTab)63 publishTiledPaneOwners(destination, destinationTab)
@@ -97,7 +97,7 @@ func TestMovePaneFinalSourceClientFollowsDestination(t *testing.T) {
97 require.NotNil(t, rc)97 require.NotNil(t, rc)
98 var rebased atomic.Bool98 var rebased atomic.Bool
99 client.renderStages.handoffRebase = func() { rebased.Store(true) }99 client.renderStages.handoffRebase = func() { rebased.Store(true) }
100- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}, active: 0}100+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination"}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}}
101 destinationTab := destination.tabs[0]101 destinationTab := destination.tabs[0]
102 destinationTab.stableID = "destination-tab"102 destinationTab.stableID = "destination-tab"
103 publishTiledPaneOwners(destination, destinationTab)103 publishTiledPaneOwners(destination, destinationTab)
@@ -117,7 +117,7 @@ func TestMovePaneFinalSourceClientFollowsDestination(t *testing.T) {
117 require.Same(t, destination, client.currentSession())117 require.Same(t, destination, client.currentSession())
118 require.Contains(t, destination.snapshotAttachments(), client)118 require.Contains(t, destination.snapshotAttachments(), client)
119 require.Equal(t, attachmentActive, destination.attachmentRole(client))119 require.Equal(t, attachmentActive, destination.attachmentRole(client))
120- require.Same(t, destinationTab, destination.tabs[destination.active])120+ require.Same(t, destinationTab, destination.tabs[testAttachmentTabIndex(destination)])
121 require.True(t, rebased.Load(), "final-source follower must rebase output before its first paint")121 require.True(t, rebased.Load(), "final-source follower must rebase output before its first paint")
122 require.NoError(t, moved.ctx.Err(), "transferred pane process must remain alive")122 require.NoError(t, moved.ctx.Err(), "transferred pane process must remain alive")
123 }123 }
@@ -140,9 +140,7 @@ func TestMovePaneRetiresEmptySourceWithoutClosingTransferredResources(t *testing
140 movedTab.stableID = "source-tab"140 movedTab.stableID = "source-tab"
141 141
142 destinationPTY := newQuietPTY()142 destinationPTY := newQuietPTY()
143- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},143+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
144- active: 0,
145- }
146 publishTiledPaneOwners(destination, destination.tabs[0])144 publishTiledPaneOwners(destination, destination.tabs[0])
147 d.mu.Lock()145 d.mu.Lock()
148 d.sessions[destination.id] = destination146 d.sessions[destination.id] = destination
@@ -190,9 +188,7 @@ func TestMovePaneRetiresSourceParkedClients(t *testing.T) {
190 parked.initOverlays()188 parked.initOverlays()
191 require.True(t, d.parkAttachment(source, parked))189 require.True(t, d.parkAttachment(source, parked))
192 destinationPTY := newQuietPTY()190 destinationPTY := newQuietPTY()
193- destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})},191+ destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
194- active: 0,
195- }
196 publishTiledPaneOwners(destination, destination.tabs[0])192 publishTiledPaneOwners(destination, destination.tabs[0])
197 d.mu.Lock()193 d.mu.Lock()
198 d.sessions[destination.id] = destination194 d.sessions[destination.id] = destination
M internal/usecase/daemon/navigation_overflow_test.go +10 -10
@@ -58,7 +58,7 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) {
5858 t.Run(tt.name, func(t *testing.T) {
5959 d, sess, ac, _ := newManualSessionWithPTYs(t, nil, nil)
6060 sess.mu.Lock()
61- sess.active = tt.start
61+ selectTestAttachmentTabLocked(sess, tt.start)
6262 target := sess.tabs[tt.wantActive]
6363 source := sess.tabs[tt.start]
6464 sess.mu.Unlock()
@@ -71,11 +71,11 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) {
7171
7272 daemonKeyHandler{d: d, ac: ac}.Action(tt.action, nil)
7373
74- require.Equal(t, tt.wantActive, activeTabIndex(sess))
74+ require.Equal(t, tt.wantActive, testAttachmentTabIndex(sess))
7575 target.mu.Lock()
7676 require.Equal(t, tt.wantFocus, target.tree.Focus)
7777 target.mu.Unlock()
78- require.NotSame(t, source, sess.activeTab())
78+ require.NotSame(t, source, testAttachmentTab(sess))
7979 })
8080 }
8181 }
@@ -93,7 +93,7 @@ func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *t
9393 target.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: focus}
9494 target.panes["pane-2"] = newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
9595 target.mu.Unlock()
96- return &session{sessionCore: sessionCore{id: domain.SessionID(id), name: name}, ctx: t.Context(), cancel: func() {}, tabs: tabs, active: active}
96+ return &session{sessionCore: sessionCore{id: domain.SessionID(id), name: name}, ctx: t.Context(), cancel: func() {}, tabs: tabs}
9797 }
9898 charlie := newSession("live-charlie", "charlie", 1, "pane-2")
9999 echo := newSession("live-echo", "echo", 1, "pane-2")
@@ -130,7 +130,7 @@ func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *t
130130 }
131131
132132 for _, target := range []*session{charlie, echo} {
133- require.Equal(t, 1, activeTabIndex(target), "switch preserves the target active tab")
133+ require.Equal(t, 1, testAttachmentTabIndex(target), "switch preserves the target active tab")
134134 target.tabs[1].mu.Lock()
135135 require.Equal(t, layout.PaneID("pane-2"), target.tabs[1].tree.Focus, "switch preserves the target pane focus")
136136 target.tabs[1].mu.Unlock()
@@ -208,7 +208,7 @@ func TestVerticalOverflowRejectsStaleSourceTab(t *testing.T) {
208208 alpha.mu.Lock()
209209 alpha.name = "alpha"
210210 expectedSource := alpha.tabs[0]
211- alpha.active = 1
211+ selectTestAttachmentTabLocked(alpha, 1)
212212 alpha.mu.Unlock()
213213 charlie := &session{sessionCore: sessionCore{id: "live-charlie", name: "charlie"}, ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}}
214214 d.mu.Lock()
@@ -351,7 +351,7 @@ func TestKeyboardHorizontalOverflowRespectsDefaultsWallsFailedEntryAndFloating(t
351351
352352 daemonKeyHandler{d: d, ac: ac}.Action(tt.dir, nil)
353353
354- require.Equal(t, 0, activeTabIndex(sess))
354+ require.Equal(t, 0, testAttachmentTabIndex(sess))
355355 })
356356 }
357357 }
@@ -363,7 +363,7 @@ func TestCommitTabOverflowRevalidatesTabPointerIdentities(t *testing.T) {
363363 }{
364364 {name: "source no longer active", mutate: func(sess *session, _ tabOverflowCandidate) {
365365 sess.mu.Lock()
366- sess.active = 1
366+ selectTestAttachmentTabLocked(sess, 1)
367367 sess.mu.Unlock()
368368 }},
369369 {name: "target entry replaced", mutate: func(sess *session, candidate tabOverflowCandidate) {
@@ -382,10 +382,10 @@ func TestCommitTabOverflowRevalidatesTabPointerIdentities(t *testing.T) {
382382 candidate, ok := d.prepareTabOverflow(sess, sess.tabs[0], layout.Right, domain.Rect{Width: 80, Height: 23}, 1)
383383 require.True(t, ok)
384384 tt.mutate(sess, candidate)
385- activeBeforeCommit := activeTabIndex(sess)
385+ activeBeforeCommit := testAttachmentTabIndex(sess)
386386
387387 require.False(t, d.commitTabOverflow(sess, candidate))
388- require.Equal(t, activeBeforeCommit, activeTabIndex(sess))
388+ require.Equal(t, activeBeforeCommit, testAttachmentTabIndex(sess))
389389 })
390390 }
391391 }
@@ -58,7 +58,7 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) {
58 t.Run(tt.name, func(t *testing.T) {58 t.Run(tt.name, func(t *testing.T) {
59 d, sess, ac, _ := newManualSessionWithPTYs(t, nil, nil)59 d, sess, ac, _ := newManualSessionWithPTYs(t, nil, nil)
60 sess.mu.Lock()60 sess.mu.Lock()
61- sess.active = tt.start61+ selectTestAttachmentTabLocked(sess, tt.start)
62 target := sess.tabs[tt.wantActive]62 target := sess.tabs[tt.wantActive]
63 source := sess.tabs[tt.start]63 source := sess.tabs[tt.start]
64 sess.mu.Unlock()64 sess.mu.Unlock()
@@ -71,11 +71,11 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) {
71 71
72 daemonKeyHandler{d: d, ac: ac}.Action(tt.action, nil)72 daemonKeyHandler{d: d, ac: ac}.Action(tt.action, nil)
73 73
74- require.Equal(t, tt.wantActive, activeTabIndex(sess))74+ require.Equal(t, tt.wantActive, testAttachmentTabIndex(sess))
75 target.mu.Lock()75 target.mu.Lock()
76 require.Equal(t, tt.wantFocus, target.tree.Focus)76 require.Equal(t, tt.wantFocus, target.tree.Focus)
77 target.mu.Unlock()77 target.mu.Unlock()
78- require.NotSame(t, source, sess.activeTab())78+ require.NotSame(t, source, testAttachmentTab(sess))
79 })79 })
80 }80 }
81 }81 }
@@ -93,7 +93,7 @@ func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *t
93 target.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: focus}93 target.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: focus}
94 target.panes["pane-2"] = newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})94 target.panes["pane-2"] = newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10})
95 target.mu.Unlock()95 target.mu.Unlock()
96- return &session{sessionCore: sessionCore{id: domain.SessionID(id), name: name}, ctx: t.Context(), cancel: func() {}, tabs: tabs, active: active}96+ return &session{sessionCore: sessionCore{id: domain.SessionID(id), name: name}, ctx: t.Context(), cancel: func() {}, tabs: tabs}
97 }97 }
98 charlie := newSession("live-charlie", "charlie", 1, "pane-2")98 charlie := newSession("live-charlie", "charlie", 1, "pane-2")
99 echo := newSession("live-echo", "echo", 1, "pane-2")99 echo := newSession("live-echo", "echo", 1, "pane-2")
@@ -130,7 +130,7 @@ func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *t
130 }130 }
131 131
132 for _, target := range []*session{charlie, echo} {132 for _, target := range []*session{charlie, echo} {
133- require.Equal(t, 1, activeTabIndex(target), "switch preserves the target active tab")133+ require.Equal(t, 1, testAttachmentTabIndex(target), "switch preserves the target active tab")
134 target.tabs[1].mu.Lock()134 target.tabs[1].mu.Lock()
135 require.Equal(t, layout.PaneID("pane-2"), target.tabs[1].tree.Focus, "switch preserves the target pane focus")135 require.Equal(t, layout.PaneID("pane-2"), target.tabs[1].tree.Focus, "switch preserves the target pane focus")
136 target.tabs[1].mu.Unlock()136 target.tabs[1].mu.Unlock()
@@ -208,7 +208,7 @@ func TestVerticalOverflowRejectsStaleSourceTab(t *testing.T) {
208 alpha.mu.Lock()208 alpha.mu.Lock()
209 alpha.name = "alpha"209 alpha.name = "alpha"
210 expectedSource := alpha.tabs[0]210 expectedSource := alpha.tabs[0]
211- alpha.active = 1211+ selectTestAttachmentTabLocked(alpha, 1)
212 alpha.mu.Unlock()212 alpha.mu.Unlock()
213 charlie := &session{sessionCore: sessionCore{id: "live-charlie", name: "charlie"}, ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}}213 charlie := &session{sessionCore: sessionCore{id: "live-charlie", name: "charlie"}, ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}}
214 d.mu.Lock()214 d.mu.Lock()
@@ -351,7 +351,7 @@ func TestKeyboardHorizontalOverflowRespectsDefaultsWallsFailedEntryAndFloating(t
351 351
352 daemonKeyHandler{d: d, ac: ac}.Action(tt.dir, nil)352 daemonKeyHandler{d: d, ac: ac}.Action(tt.dir, nil)
353 353
354- require.Equal(t, 0, activeTabIndex(sess))354+ require.Equal(t, 0, testAttachmentTabIndex(sess))
355 })355 })
356 }356 }
357 }357 }
@@ -363,7 +363,7 @@ func TestCommitTabOverflowRevalidatesTabPointerIdentities(t *testing.T) {
363 }{363 }{
364 {name: "source no longer active", mutate: func(sess *session, _ tabOverflowCandidate) {364 {name: "source no longer active", mutate: func(sess *session, _ tabOverflowCandidate) {
365 sess.mu.Lock()365 sess.mu.Lock()
366- sess.active = 1366+ selectTestAttachmentTabLocked(sess, 1)
367 sess.mu.Unlock()367 sess.mu.Unlock()
368 }},368 }},
369 {name: "target entry replaced", mutate: func(sess *session, candidate tabOverflowCandidate) {369 {name: "target entry replaced", mutate: func(sess *session, candidate tabOverflowCandidate) {
@@ -382,10 +382,10 @@ func TestCommitTabOverflowRevalidatesTabPointerIdentities(t *testing.T) {
382 candidate, ok := d.prepareTabOverflow(sess, sess.tabs[0], layout.Right, domain.Rect{Width: 80, Height: 23}, 1)382 candidate, ok := d.prepareTabOverflow(sess, sess.tabs[0], layout.Right, domain.Rect{Width: 80, Height: 23}, 1)
383 require.True(t, ok)383 require.True(t, ok)
384 tt.mutate(sess, candidate)384 tt.mutate(sess, candidate)
385- activeBeforeCommit := activeTabIndex(sess)385+ activeBeforeCommit := testAttachmentTabIndex(sess)
386 386
387 require.False(t, d.commitTabOverflow(sess, candidate))387 require.False(t, d.commitTabOverflow(sess, candidate))
388- require.Equal(t, activeBeforeCommit, activeTabIndex(sess))388+ require.Equal(t, activeBeforeCommit, testAttachmentTabIndex(sess))
389 })389 })
390 }390 }
391 }391 }
M internal/usecase/daemon/navigation_role_handoff_test.go +1 -1
@@ -136,7 +136,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
136136 source.mu.Unlock()
137137 target.mu.Lock()
138138 require.Empty(t, target.snapshotAttachmentsLocked(), "stale handoff attached to the target")
139- require.Zero(t, target.active, "stale handoff changed target tab focus")
139+ require.Zero(t, testAttachmentTabIndexLocked(target), "stale handoff changed target tab focus")
140140 target.mu.Unlock()
141141 if tt.check != nil {
142142 tt.check(t, d, source, target)
@@ -136,7 +136,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
136 source.mu.Unlock()136 source.mu.Unlock()
137 target.mu.Lock()137 target.mu.Lock()
138 require.Empty(t, target.snapshotAttachmentsLocked(), "stale handoff attached to the target")138 require.Empty(t, target.snapshotAttachmentsLocked(), "stale handoff attached to the target")
139- require.Zero(t, target.active, "stale handoff changed target tab focus")139+ require.Zero(t, testAttachmentTabIndexLocked(target), "stale handoff changed target tab focus")
140 target.mu.Unlock()140 target.mu.Unlock()
141 if tt.check != nil {141 if tt.check != nil {
142 tt.check(t, d, source, target)142 tt.check(t, d, source, target)
M internal/usecase/daemon/navigation_role_incarnation_test.go +1 -1
@@ -74,7 +74,7 @@ func TestNavigationHandoffRejectsResumedOrReboundInitiatorIncarnation(t *testing
7474 require.Same(t, source, ac.currentSession())
7575 target.mu.Lock()
7676 require.Empty(t, target.snapshotAttachmentsLocked())
77- require.Zero(t, target.active)
77+ require.Zero(t, testAttachmentTabIndexLocked(target))
7878 target.mu.Unlock()
7979 })
8080 }
@@ -74,7 +74,7 @@ func TestNavigationHandoffRejectsResumedOrReboundInitiatorIncarnation(t *testing
74 require.Same(t, source, ac.currentSession())74 require.Same(t, source, ac.currentSession())
75 target.mu.Lock()75 target.mu.Lock()
76 require.Empty(t, target.snapshotAttachmentsLocked())76 require.Empty(t, target.snapshotAttachmentsLocked())
77- require.Zero(t, target.active)77+ require.Zero(t, testAttachmentTabIndexLocked(target))
78 target.mu.Unlock()78 target.mu.Unlock()
79 })79 })
80 }80 }
M internal/usecase/daemon/output_state_sync_test.go +3 -3
@@ -285,7 +285,7 @@ func TestResizeGrowthFirstFrameIncludesConcurrentPTYRedraw(t *testing.T) {
285285 p := newResizeReaderPTY([]byte("\x1b[1;81H" + strings.Repeat("B", 40)))
286286 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
287287 ac.output.maxOutstanding = tc.window
288- pane := sess.activeTab().focusedPane()
288+ pane := testAttachmentTab(sess).focusedPane()
289289 p.applying = func() bool {
290290 pane.mu.Lock()
291291 defer pane.mu.Unlock()
@@ -299,7 +299,7 @@ func TestResizeGrowthFirstFrameIncludesConcurrentPTYRedraw(t *testing.T) {
299299
300300 d.sessWg.Add(1)
301301 pane.onExit = func() {}
302- go d.ptyReader(sess, sess.activeTab(), pane)
302+ go d.ptyReader(sess, testAttachmentTab(sess), pane)
303303 defer func() { p.close(); d.sessWg.Wait() }()
304304
305305 // The resize deadline drives coordinator prepare/apply/commit. No test
@@ -387,7 +387,7 @@ func TestResizeBurstFlushesOnlyLatestGeometry(t *testing.T) {
387387 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")
388388 awaitFrame(t, sends, ports.MsgOutput)
389389 require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)
390- require.Equal(t, 120, sess.activeTab().focusedPane().screen.Frame.Width)
390+ require.Equal(t, 120, testAttachmentTab(sess).focusedPane().screen.Frame.Width)
391391 requireNoOutputFrame(t, sends)
392392 }
393393
@@ -285,7 +285,7 @@ func TestResizeGrowthFirstFrameIncludesConcurrentPTYRedraw(t *testing.T) {
285 p := newResizeReaderPTY([]byte("\x1b[1;81H" + strings.Repeat("B", 40)))285 p := newResizeReaderPTY([]byte("\x1b[1;81H" + strings.Repeat("B", 40)))
286 d, sess, ac, sends := newManualSessionWithPTYs(t, p)286 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
287 ac.output.maxOutstanding = tc.window287 ac.output.maxOutstanding = tc.window
288- pane := sess.activeTab().focusedPane()288+ pane := testAttachmentTab(sess).focusedPane()
289 p.applying = func() bool {289 p.applying = func() bool {
290 pane.mu.Lock()290 pane.mu.Lock()
291 defer pane.mu.Unlock()291 defer pane.mu.Unlock()
@@ -299,7 +299,7 @@ func TestResizeGrowthFirstFrameIncludesConcurrentPTYRedraw(t *testing.T) {
299 299
300 d.sessWg.Add(1)300 d.sessWg.Add(1)
301 pane.onExit = func() {}301 pane.onExit = func() {}
302- go d.ptyReader(sess, sess.activeTab(), pane)302+ go d.ptyReader(sess, testAttachmentTab(sess), pane)
303 defer func() { p.close(); d.sessWg.Wait() }()303 defer func() { p.close(); d.sessWg.Wait() }()
304 304
305 // The resize deadline drives coordinator prepare/apply/commit. No test305 // The resize deadline drives coordinator prepare/apply/commit. No test
@@ -387,7 +387,7 @@ func TestResizeBurstFlushesOnlyLatestGeometry(t *testing.T) {
387 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")387 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")
388 awaitFrame(t, sends, ports.MsgOutput)388 awaitFrame(t, sends, ports.MsgOutput)
389 require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)389 require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)
390- require.Equal(t, 120, sess.activeTab().focusedPane().screen.Frame.Width)390+ require.Equal(t, 120, testAttachmentTab(sess).focusedPane().screen.Frame.Width)
391 requireNoOutputFrame(t, sends)391 requireNoOutputFrame(t, sends)
392 }392 }
393 393
M internal/usecase/daemon/palette.go +4 -1
@@ -672,7 +672,10 @@ func (e paletteExec) RenameSession() error {
672672 }
673673
674674 func (e paletteExec) RenameTab() error {
675- tb := e.sess.tabForAttachmentOrActive(e.ac)
675+ tb := e.sess.tabForAttachment(e.ac)
676+ if e.ac == nil {
677+ tb = e.sess.firstTab()
678+ }
676679 if tb == nil {
677680 return nil
678681 }
@@ -672,7 +672,10 @@ func (e paletteExec) RenameSession() error {
672 }672 }
673 673
674 func (e paletteExec) RenameTab() error {674 func (e paletteExec) RenameTab() error {
675- tb := e.sess.tabForAttachmentOrActive(e.ac)675+ tb := e.sess.tabForAttachment(e.ac)
676+ if e.ac == nil {
677+ tb = e.sess.firstTab()
678+ }
676 if tb == nil {679 if tb == nil {
677 return nil680 return nil
678 }681 }
M internal/usecase/daemon/palette_test.go +6 -6
@@ -109,8 +109,8 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
109109
110110 d.handleInput(sess, ac, []byte("NXT\r"))
111111 require.False(t, ac.overlays.paletteActive())
112- require.Equal(t, 1, activeTabIndex(sess))
113- requireFloatingInitialized(t, sess.activeTab())
112+ require.Equal(t, 1, testAttachmentTabIndex(sess))
113+ requireFloatingInitialized(t, testAttachmentTab(sess))
114114 awaitFrame(t, sends, ports.MsgOutput)
115115
116116 d.handleInput(sess, ac, []byte("\x1b "))
@@ -440,7 +440,7 @@ func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) {
440440 d.handleInput(sess, ac, []byte("next\r"))
441441
442442 require.False(t, ac.overlays.paletteActive())
443- require.Equal(t, 1, activeTabIndex(sess))
443+ require.Equal(t, 1, testAttachmentTabIndex(sess))
444444 awaitFrame(t, sends, ports.MsgOutput)
445445 }
446446
@@ -639,7 +639,7 @@ func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
639639 p, release := newBlockingPTY(t)
640640 defer release()
641641 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
642- tb := sess.activeTab()
642+ tb := testAttachmentTab(sess)
643643 installTestFloating(tb, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
644644
645645 d.handleInput(sess, ac, []byte("\x1b "))
@@ -1125,9 +1125,9 @@ func TestPaletteExecMethods(t *testing.T) {
11251125 exec := paletteExec{d: d, sess: sess, ac: ac}
11261126
11271127 require.NoError(t, exec.NextTab())
1128- require.Equal(t, 1, activeTabIndex(sess))
1128+ require.Equal(t, 1, testAttachmentTabIndex(sess))
11291129 require.NoError(t, exec.PrevTab())
1130- require.Equal(t, 0, activeTabIndex(sess))
1130+ require.Equal(t, 0, testAttachmentTabIndex(sess))
11311131 sess.ephemeral = true
11321132 require.NoError(t, exec.RenameSession())
11331133 require.True(t, ac.overlays.promptActive())
@@ -109,8 +109,8 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
109 109
110 d.handleInput(sess, ac, []byte("NXT\r"))110 d.handleInput(sess, ac, []byte("NXT\r"))
111 require.False(t, ac.overlays.paletteActive())111 require.False(t, ac.overlays.paletteActive())
112- require.Equal(t, 1, activeTabIndex(sess))112+ require.Equal(t, 1, testAttachmentTabIndex(sess))
113- requireFloatingInitialized(t, sess.activeTab())113+ requireFloatingInitialized(t, testAttachmentTab(sess))
114 awaitFrame(t, sends, ports.MsgOutput)114 awaitFrame(t, sends, ports.MsgOutput)
115 115
116 d.handleInput(sess, ac, []byte("\x1b "))116 d.handleInput(sess, ac, []byte("\x1b "))
@@ -440,7 +440,7 @@ func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) {
440 d.handleInput(sess, ac, []byte("next\r"))440 d.handleInput(sess, ac, []byte("next\r"))
441 441
442 require.False(t, ac.overlays.paletteActive())442 require.False(t, ac.overlays.paletteActive())
443- require.Equal(t, 1, activeTabIndex(sess))443+ require.Equal(t, 1, testAttachmentTabIndex(sess))
444 awaitFrame(t, sends, ports.MsgOutput)444 awaitFrame(t, sends, ports.MsgOutput)
445 }445 }
446 446
@@ -639,7 +639,7 @@ func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
639 p, release := newBlockingPTY(t)639 p, release := newBlockingPTY(t)
640 defer release()640 defer release()
641 d, sess, ac, sends := newManualSessionWithPTYs(t, p)641 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
642- tb := sess.activeTab()642+ tb := testAttachmentTab(sess)
643 installTestFloating(tb, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)643 installTestFloating(tb, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
644 644
645 d.handleInput(sess, ac, []byte("\x1b "))645 d.handleInput(sess, ac, []byte("\x1b "))
@@ -1125,9 +1125,9 @@ func TestPaletteExecMethods(t *testing.T) {
1125 exec := paletteExec{d: d, sess: sess, ac: ac}1125 exec := paletteExec{d: d, sess: sess, ac: ac}
1126 1126
1127 require.NoError(t, exec.NextTab())1127 require.NoError(t, exec.NextTab())
1128- require.Equal(t, 1, activeTabIndex(sess))1128+ require.Equal(t, 1, testAttachmentTabIndex(sess))
1129 require.NoError(t, exec.PrevTab())1129 require.NoError(t, exec.PrevTab())
1130- require.Equal(t, 0, activeTabIndex(sess))1130+ require.Equal(t, 0, testAttachmentTabIndex(sess))
1131 sess.ephemeral = true1131 sess.ephemeral = true
1132 require.NoError(t, exec.RenameSession())1132 require.NoError(t, exec.RenameSession())
1133 require.True(t, ac.overlays.promptActive())1133 require.True(t, ac.overlays.promptActive())
M internal/usecase/daemon/pane_actions_test.go +24 -24
@@ -78,7 +78,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
7878 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
7979 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
8080 d.clock = clock
81- tb := sess.activeTab()
81+ tb := testAttachmentTab(sess)
8282 tb.mu.Lock()
8383 tb.size = domain.Size{Cols: 100, Rows: 20}
8484 tb.bumpLayoutGenerationLocked()
@@ -118,7 +118,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
118118 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
119119 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
120120 d.clock = clock
121- tb := sess.activeTab()
121+ tb := testAttachmentTab(sess)
122122 tb.mu.Lock()
123123 tb.size = domain.Size{Cols: 100, Rows: 20}
124124 tb.bumpLayoutGenerationLocked()
@@ -145,7 +145,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
145145 func TestFocusDirAtDoesNotApplyUncommittedCandidate(t *testing.T) {
146146 pty := &transactionalResizePTY{}
147147 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
148- tb := sess.activeTab()
148+ tb := testAttachmentTab(sess)
149149 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})
150150 tb.mu.Lock()
151151 tb.panes[second.id] = second
@@ -164,7 +164,7 @@ func TestLayoutApplicationRetriesOnlyAcceptedFailedGeometry(t *testing.T) {
164164 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
165165 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
166166 d.clock = clock
167- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
167+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
168168 tb.mu.Lock()
169169 tb.size = domain.Size{Cols: 100, Rows: 20}
170170 tb.bumpLayoutGenerationLocked()
@@ -206,7 +206,7 @@ func TestDelayedRetryUsesFreshLayoutAfterPaneResize(t *testing.T) {
206206 failed := &transactionalResizePTY{errs: []error{errors.New("first resize fails")}}
207207 d, sess, ac, _ := newManualSessionWithPTYs(t, failed)
208208 d.clock = clock.clock
209- tb := sess.activeTab()
209+ tb := testAttachmentTab(sess)
210210 first := tb.focusedPane()
211211 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})
212212 tb.mu.Lock()
@@ -260,7 +260,7 @@ func TestDelayedRetryUsesFreshLayoutAfterPaneResize(t *testing.T) {
260260
261261 func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) {
262262 d, sess, pty, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
263- tb := sess.activeTab()
263+ tb := testAttachmentTab(sess)
264264 tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10}
265265 entered := make(chan struct{})
266266 release := make(chan struct{})
@@ -290,7 +290,7 @@ func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) {
290290
291291 func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) {
292292 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
293- tb := sess.activeTab()
293+ tb := testAttachmentTab(sess)
294294 oldPane := tb.focusedPane()
295295 oldPane.rect = domain.Rect{Width: 20, Height: 10}
296296 oldPane.screen.Resize(20, 10)
@@ -341,7 +341,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
341341 first, entered, release := newBlockedPTY()
342342 second := &transactionalResizePTY{}
343343 d, sess, _, _ := newManualSessionWithPTYs(t, first)
344- tb := sess.activeTab()
344+ tb := testAttachmentTab(sess)
345345 p1 := tb.focusedPane()
346346 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})
347347 tb.mu.Lock()
@@ -383,7 +383,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
383383 first, entered, release := newBlockedPTY()
384384 second := &transactionalResizePTY{}
385385 d, sess, _, _ := newManualSessionWithPTYs(t, first)
386- tb := sess.activeTab()
386+ tb := testAttachmentTab(sess)
387387 p1 := tb.focusedPane()
388388 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})
389389 tb.mu.Lock()
@@ -417,7 +417,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
417417 t.Run("client size rejects and retries a blocked plan", func(t *testing.T) {
418418 pty, entered, release := newBlockedPTY()
419419 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
420- tb := sess.activeTab()
420+ tb := testAttachmentTab(sess)
421421 p := tb.focusedPane()
422422 p.rect = domain.Rect{Width: 20, Height: 10}
423423
@@ -476,7 +476,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) {
476476
477477 require.NoError(t, d.splitPane(sess, nil, tt.dir))
478478
479- tb := sess.activeTab()
479+ tb := testAttachmentTab(sess)
480480 tb.mu.Lock()
481481 defer tb.mu.Unlock()
482482 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)
@@ -492,7 +492,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) {
492492
493493 func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) {
494494 d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 4})
495- tb := sess.activeTab()
495+ tb := testAttachmentTab(sess)
496496 stackPTY := portsmocks.NewMockPTY(t)
497497 stackPane := newPane("pane-2", stackPTY, domain.Size{Cols: 41, Rows: 2})
498498 tb.panes[stackPane.id] = stackPane
@@ -520,7 +520,7 @@ func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) {
520520 cause := errors.New("open failed")
521521 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, cause).Once()
522522
523- tb := sess.activeTab()
523+ tb := testAttachmentTab(sess)
524524 tb.mu.Lock()
525525 beforeRoot, beforeFocus, beforeNext, beforePaneCount := tb.tree.Root.Leaf, tb.tree.Focus, tb.nextPaneID, len(tb.panes)
526526 tb.mu.Unlock()
@@ -547,7 +547,7 @@ func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) {
547547
548548 func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) {
549549 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
550- tb := sess.activeTab()
550+ tb := testAttachmentTab(sess)
551551 newPTY := portsmocks.NewMockPTY(t)
552552 newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10})
553553 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -564,7 +564,7 @@ func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) {
564564
565565 func TestResizeReflowsAllPanes(t *testing.T) {
566566 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
567- tb := sess.activeTab()
567+ tb := testAttachmentTab(sess)
568568 newPTY := portsmocks.NewMockPTY(t)
569569 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
570570 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})
@@ -596,7 +596,7 @@ func TestFocusDirAtReturnsDepartingSpanAndMapsNoNeighbor(t *testing.T) {
596596 for _, tt := range tests {
597597 t.Run(tt.name, func(t *testing.T) {
598598 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
599- tb := sess.activeTab()
599+ tb := testAttachmentTab(sess)
600600 target := tb.panes["pane-1"]
601601 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}
602602 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}
@@ -614,7 +614,7 @@ func TestFocusDirAtReturnsDepartingSpanAndMapsNoNeighbor(t *testing.T) {
614614
615615 func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
616616 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
617- tb := sess.activeTab()
617+ tb := testAttachmentTab(sess)
618618 newPTY := portsmocks.NewMockPTY(t)
619619 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}
620620 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})
@@ -635,7 +635,7 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
635635
636636 func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) {
637637 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
638- tb := sess.activeTab()
638+ tb := testAttachmentTab(sess)
639639 closingPTY := portsmocks.NewMockPTY(t)
640640 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
641641 tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10})
@@ -654,7 +654,7 @@ func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) {
654654
655655 func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) {
656656 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
657- tb := sess.activeTab()
657+ tb := testAttachmentTab(sess)
658658 reapedPTY := portsmocks.NewMockPTY(t)
659659 reaped := newPane("pane-2", reapedPTY, domain.Size{Cols: 20, Rows: 10})
660660 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -673,7 +673,7 @@ func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) {
673673
674674 func TestClosePaneKeepsTabOpenWhenPendingSpawnLeafExists(t *testing.T) {
675675 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
676- tb := sess.activeTab()
676+ tb := testAttachmentTab(sess)
677677 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
678678 oldPTY.EXPECT().Close().Return(nil).Once()
679679
@@ -705,7 +705,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) {
705705 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once()
706706
707707 require.NoError(t, d.stackPane(sess, nil))
708- tb := sess.activeTab()
708+ tb := testAttachmentTab(sess)
709709 require.Equal(t, layout.Stack, tb.tree.Root.Kind)
710710 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)
711711 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded)
@@ -728,7 +728,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) {
728728
729729 require.NoError(t, d.focusDir(sess, nil, layout.Up, nil))
730730
731- tb := sess.activeTab()
731+ tb := testAttachmentTab(sess)
732732 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus)
733733 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded)
734734 thirdPTY := portsmocks.NewMockPTY(t)
@@ -773,7 +773,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) {
773773 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once()
774774
775775 require.NoError(t, d.splitPane(sess, nil, layout.Right))
776- tb := sess.activeTab()
776+ tb := testAttachmentTab(sess)
777777 tb.mu.Lock()
778778 tb.tree.Focus = "pane-1"
779779 tb.mu.Unlock()
@@ -844,7 +844,7 @@ func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) {
844844 ).Once()
845845
846846 require.NoError(t, d.splitPane(sess, nil, layout.Right))
847- tb := sess.activeTab()
847+ tb := testAttachmentTab(sess)
848848 tb.mu.Lock()
849849 paneID := tb.panes["pane-2"].stableID
850850 tabID := tb.stableID
@@ -78,7 +78,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
78 d, sess, _, _ := newManualSessionWithPTYs(t, pty)78 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
79 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}79 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
80 d.clock = clock80 d.clock = clock
81- tb := sess.activeTab()81+ tb := testAttachmentTab(sess)
82 tb.mu.Lock()82 tb.mu.Lock()
83 tb.size = domain.Size{Cols: 100, Rows: 20}83 tb.size = domain.Size{Cols: 100, Rows: 20}
84 tb.bumpLayoutGenerationLocked()84 tb.bumpLayoutGenerationLocked()
@@ -118,7 +118,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
118 d, sess, _, _ := newManualSessionWithPTYs(t, pty)118 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
119 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}119 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
120 d.clock = clock120 d.clock = clock
121- tb := sess.activeTab()121+ tb := testAttachmentTab(sess)
122 tb.mu.Lock()122 tb.mu.Lock()
123 tb.size = domain.Size{Cols: 100, Rows: 20}123 tb.size = domain.Size{Cols: 100, Rows: 20}
124 tb.bumpLayoutGenerationLocked()124 tb.bumpLayoutGenerationLocked()
@@ -145,7 +145,7 @@ func TestLayoutRetryIsBoundedDeduplicatedAndCanceled(t *testing.T) {
145 func TestFocusDirAtDoesNotApplyUncommittedCandidate(t *testing.T) {145 func TestFocusDirAtDoesNotApplyUncommittedCandidate(t *testing.T) {
146 pty := &transactionalResizePTY{}146 pty := &transactionalResizePTY{}
147 d, sess, _, _ := newManualSessionWithPTYs(t, pty)147 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
148- tb := sess.activeTab()148+ tb := testAttachmentTab(sess)
149 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})149 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})
150 tb.mu.Lock()150 tb.mu.Lock()
151 tb.panes[second.id] = second151 tb.panes[second.id] = second
@@ -164,7 +164,7 @@ func TestLayoutApplicationRetriesOnlyAcceptedFailedGeometry(t *testing.T) {
164 d, sess, _, _ := newManualSessionWithPTYs(t, pty)164 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
165 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}165 clock := &layoutRetryClock{timers: make(chan *layoutRetryTimer, 16)}
166 d.clock = clock166 d.clock = clock
167- tb, p := sess.activeTab(), sess.activeTab().focusedPane()167+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
168 tb.mu.Lock()168 tb.mu.Lock()
169 tb.size = domain.Size{Cols: 100, Rows: 20}169 tb.size = domain.Size{Cols: 100, Rows: 20}
170 tb.bumpLayoutGenerationLocked()170 tb.bumpLayoutGenerationLocked()
@@ -206,7 +206,7 @@ func TestDelayedRetryUsesFreshLayoutAfterPaneResize(t *testing.T) {
206 failed := &transactionalResizePTY{errs: []error{errors.New("first resize fails")}}206 failed := &transactionalResizePTY{errs: []error{errors.New("first resize fails")}}
207 d, sess, ac, _ := newManualSessionWithPTYs(t, failed)207 d, sess, ac, _ := newManualSessionWithPTYs(t, failed)
208 d.clock = clock.clock208 d.clock = clock.clock
209- tb := sess.activeTab()209+ tb := testAttachmentTab(sess)
210 first := tb.focusedPane()210 first := tb.focusedPane()
211 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})211 second := newPane("pane-2", nil, domain.Size{Cols: 80, Rows: 23})
212 tb.mu.Lock()212 tb.mu.Lock()
@@ -260,7 +260,7 @@ func TestDelayedRetryUsesFreshLayoutAfterPaneResize(t *testing.T) {
260 260
261 func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) {261 func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) {
262 d, sess, pty, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})262 d, sess, pty, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
263- tb := sess.activeTab()263+ tb := testAttachmentTab(sess)
264 tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10}264 tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10}
265 entered := make(chan struct{})265 entered := make(chan struct{})
266 release := make(chan struct{})266 release := make(chan struct{})
@@ -290,7 +290,7 @@ func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) {
290 290
291 func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) {291 func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) {
292 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})292 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
293- tb := sess.activeTab()293+ tb := testAttachmentTab(sess)
294 oldPane := tb.focusedPane()294 oldPane := tb.focusedPane()
295 oldPane.rect = domain.Rect{Width: 20, Height: 10}295 oldPane.rect = domain.Rect{Width: 20, Height: 10}
296 oldPane.screen.Resize(20, 10)296 oldPane.screen.Resize(20, 10)
@@ -341,7 +341,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
341 first, entered, release := newBlockedPTY()341 first, entered, release := newBlockedPTY()
342 second := &transactionalResizePTY{}342 second := &transactionalResizePTY{}
343 d, sess, _, _ := newManualSessionWithPTYs(t, first)343 d, sess, _, _ := newManualSessionWithPTYs(t, first)
344- tb := sess.activeTab()344+ tb := testAttachmentTab(sess)
345 p1 := tb.focusedPane()345 p1 := tb.focusedPane()
346 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})346 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})
347 tb.mu.Lock()347 tb.mu.Lock()
@@ -383,7 +383,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
383 first, entered, release := newBlockedPTY()383 first, entered, release := newBlockedPTY()
384 second := &transactionalResizePTY{}384 second := &transactionalResizePTY{}
385 d, sess, _, _ := newManualSessionWithPTYs(t, first)385 d, sess, _, _ := newManualSessionWithPTYs(t, first)
386- tb := sess.activeTab()386+ tb := testAttachmentTab(sess)
387 p1 := tb.focusedPane()387 p1 := tb.focusedPane()
388 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})388 p2 := newPane("pane-2", second, domain.Size{Cols: 80, Rows: 23})
389 tb.mu.Lock()389 tb.mu.Lock()
@@ -417,7 +417,7 @@ func TestLayoutApplicationRejectsStaleCloseFocusAndSize(t *testing.T) {
417 t.Run("client size rejects and retries a blocked plan", func(t *testing.T) {417 t.Run("client size rejects and retries a blocked plan", func(t *testing.T) {
418 pty, entered, release := newBlockedPTY()418 pty, entered, release := newBlockedPTY()
419 d, sess, _, _ := newManualSessionWithPTYs(t, pty)419 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
420- tb := sess.activeTab()420+ tb := testAttachmentTab(sess)
421 p := tb.focusedPane()421 p := tb.focusedPane()
422 p.rect = domain.Rect{Width: 20, Height: 10}422 p.rect = domain.Rect{Width: 20, Height: 10}
423 423
@@ -476,7 +476,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) {
476 476
477 require.NoError(t, d.splitPane(sess, nil, tt.dir))477 require.NoError(t, d.splitPane(sess, nil, tt.dir))
478 478
479- tb := sess.activeTab()479+ tb := testAttachmentTab(sess)
480 tb.mu.Lock()480 tb.mu.Lock()
481 defer tb.mu.Unlock()481 defer tb.mu.Unlock()
482 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)482 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)
@@ -492,7 +492,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) {
492 492
493 func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) {493 func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) {
494 d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 4})494 d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 4})
495- tb := sess.activeTab()495+ tb := testAttachmentTab(sess)
496 stackPTY := portsmocks.NewMockPTY(t)496 stackPTY := portsmocks.NewMockPTY(t)
497 stackPane := newPane("pane-2", stackPTY, domain.Size{Cols: 41, Rows: 2})497 stackPane := newPane("pane-2", stackPTY, domain.Size{Cols: 41, Rows: 2})
498 tb.panes[stackPane.id] = stackPane498 tb.panes[stackPane.id] = stackPane
@@ -520,7 +520,7 @@ func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) {
520 cause := errors.New("open failed")520 cause := errors.New("open failed")
521 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, cause).Once()521 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, cause).Once()
522 522
523- tb := sess.activeTab()523+ tb := testAttachmentTab(sess)
524 tb.mu.Lock()524 tb.mu.Lock()
525 beforeRoot, beforeFocus, beforeNext, beforePaneCount := tb.tree.Root.Leaf, tb.tree.Focus, tb.nextPaneID, len(tb.panes)525 beforeRoot, beforeFocus, beforeNext, beforePaneCount := tb.tree.Root.Leaf, tb.tree.Focus, tb.nextPaneID, len(tb.panes)
526 tb.mu.Unlock()526 tb.mu.Unlock()
@@ -547,7 +547,7 @@ func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) {
547 547
548 func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) {548 func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) {
549 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})549 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
550- tb := sess.activeTab()550+ tb := testAttachmentTab(sess)
551 newPTY := portsmocks.NewMockPTY(t)551 newPTY := portsmocks.NewMockPTY(t)
552 newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10})552 newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10})
553 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}553 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -564,7 +564,7 @@ func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) {
564 564
565 func TestResizeReflowsAllPanes(t *testing.T) {565 func TestResizeReflowsAllPanes(t *testing.T) {
566 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})566 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
567- tb := sess.activeTab()567+ tb := testAttachmentTab(sess)
568 newPTY := portsmocks.NewMockPTY(t)568 newPTY := portsmocks.NewMockPTY(t)
569 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}569 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
570 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})570 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})
@@ -596,7 +596,7 @@ func TestFocusDirAtReturnsDepartingSpanAndMapsNoNeighbor(t *testing.T) {
596 for _, tt := range tests {596 for _, tt := range tests {
597 t.Run(tt.name, func(t *testing.T) {597 t.Run(tt.name, func(t *testing.T) {
598 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})598 d, sess, _, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
599- tb := sess.activeTab()599+ tb := testAttachmentTab(sess)
600 target := tb.panes["pane-1"]600 target := tb.panes["pane-1"]
601 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}601 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}
602 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}602 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}
@@ -614,7 +614,7 @@ func TestFocusDirAtReturnsDepartingSpanAndMapsNoNeighbor(t *testing.T) {
614 614
615 func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {615 func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
616 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})616 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
617- tb := sess.activeTab()617+ tb := testAttachmentTab(sess)
618 newPTY := portsmocks.NewMockPTY(t)618 newPTY := portsmocks.NewMockPTY(t)
619 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}619 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-1"}
620 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})620 tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10})
@@ -635,7 +635,7 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
635 635
636 func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) {636 func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) {
637 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})637 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
638- tb := sess.activeTab()638+ tb := testAttachmentTab(sess)
639 closingPTY := portsmocks.NewMockPTY(t)639 closingPTY := portsmocks.NewMockPTY(t)
640 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}640 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
641 tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10})641 tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10})
@@ -654,7 +654,7 @@ func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) {
654 654
655 func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) {655 func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) {
656 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})656 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
657- tb := sess.activeTab()657+ tb := testAttachmentTab(sess)
658 reapedPTY := portsmocks.NewMockPTY(t)658 reapedPTY := portsmocks.NewMockPTY(t)
659 reaped := newPane("pane-2", reapedPTY, domain.Size{Cols: 20, Rows: 10})659 reaped := newPane("pane-2", reapedPTY, domain.Size{Cols: 20, Rows: 10})
660 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}660 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -673,7 +673,7 @@ func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) {
673 673
674 func TestClosePaneKeepsTabOpenWhenPendingSpawnLeafExists(t *testing.T) {674 func TestClosePaneKeepsTabOpenWhenPendingSpawnLeafExists(t *testing.T) {
675 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})675 d, sess, oldPTY, _ := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10})
676- tb := sess.activeTab()676+ tb := testAttachmentTab(sess)
677 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}677 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
678 oldPTY.EXPECT().Close().Return(nil).Once()678 oldPTY.EXPECT().Close().Return(nil).Once()
679 679
@@ -705,7 +705,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) {
705 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once()705 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once()
706 706
707 require.NoError(t, d.stackPane(sess, nil))707 require.NoError(t, d.stackPane(sess, nil))
708- tb := sess.activeTab()708+ tb := testAttachmentTab(sess)
709 require.Equal(t, layout.Stack, tb.tree.Root.Kind)709 require.Equal(t, layout.Stack, tb.tree.Root.Kind)
710 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)710 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus)
711 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded)711 require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded)
@@ -728,7 +728,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) {
728 728
729 require.NoError(t, d.focusDir(sess, nil, layout.Up, nil))729 require.NoError(t, d.focusDir(sess, nil, layout.Up, nil))
730 730
731- tb := sess.activeTab()731+ tb := testAttachmentTab(sess)
732 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus)732 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus)
733 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded)733 require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded)
734 thirdPTY := portsmocks.NewMockPTY(t)734 thirdPTY := portsmocks.NewMockPTY(t)
@@ -773,7 +773,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) {
773 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once()773 factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once()
774 774
775 require.NoError(t, d.splitPane(sess, nil, layout.Right))775 require.NoError(t, d.splitPane(sess, nil, layout.Right))
776- tb := sess.activeTab()776+ tb := testAttachmentTab(sess)
777 tb.mu.Lock()777 tb.mu.Lock()
778 tb.tree.Focus = "pane-1"778 tb.tree.Focus = "pane-1"
779 tb.mu.Unlock()779 tb.mu.Unlock()
@@ -844,7 +844,7 @@ func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) {
844 ).Once()844 ).Once()
845 845
846 require.NoError(t, d.splitPane(sess, nil, layout.Right))846 require.NoError(t, d.splitPane(sess, nil, layout.Right))
847- tb := sess.activeTab()847+ tb := testAttachmentTab(sess)
848 tb.mu.Lock()848 tb.mu.Lock()
849 paneID := tb.panes["pane-2"].stableID849 paneID := tb.panes["pane-2"].stableID
850 tabID := tb.stableID850 tabID := tb.stableID
M internal/usecase/daemon/pane_lifetime_test.go +2 -2
@@ -223,7 +223,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
223223 setup: func(t *testing.T, d *Daemon) (*pane, func(), func()) {
224224 sess, err := createSessionForTest(d, "work", true, "/tmp", domain.Size{Cols: 80, Rows: 24}, terminalEnv{}, nil)
225225 require.NoError(t, err)
226- return sess.activeTab().focusedPane(), sess.cancel, func() {
226+ return testAttachmentTab(sess).focusedPane(), sess.cancel, func() {
227227 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
228228 }
229229 },
@@ -239,7 +239,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
239239 d.sessions[sess.id] = sess
240240 d.mu.Unlock()
241241 require.NoError(t, d.createTab(sess, domain.Size{Cols: 80, Rows: 24}))
242- created := sess.activeTab()
242+ created := testAttachmentTab(sess)
243243 return created.focusedPane(), cancelSession, created.closeAllPanes
244244 },
245245 },
@@ -223,7 +223,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
223 setup: func(t *testing.T, d *Daemon) (*pane, func(), func()) {223 setup: func(t *testing.T, d *Daemon) (*pane, func(), func()) {
224 sess, err := createSessionForTest(d, "work", true, "/tmp", domain.Size{Cols: 80, Rows: 24}, terminalEnv{}, nil)224 sess, err := createSessionForTest(d, "work", true, "/tmp", domain.Size{Cols: 80, Rows: 24}, terminalEnv{}, nil)
225 require.NoError(t, err)225 require.NoError(t, err)
226- return sess.activeTab().focusedPane(), sess.cancel, func() {226+ return testAttachmentTab(sess).focusedPane(), sess.cancel, func() {
227 _ = d.killSession(sess, ports.ReasonSessionKilled, false)227 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
228 }228 }
229 },229 },
@@ -239,7 +239,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
239 d.sessions[sess.id] = sess239 d.sessions[sess.id] = sess
240 d.mu.Unlock()240 d.mu.Unlock()
241 require.NoError(t, d.createTab(sess, domain.Size{Cols: 80, Rows: 24}))241 require.NoError(t, d.createTab(sess, domain.Size{Cols: 80, Rows: 24}))
242- created := sess.activeTab()242+ created := testAttachmentTab(sess)
243 return created.focusedPane(), cancelSession, created.closeAllPanes243 return created.focusedPane(), cancelSession, created.closeAllPanes
244 },244 },
245 },245 },
M internal/usecase/daemon/pane_owner_reader_test.go +4 -4
@@ -74,7 +74,7 @@ func (p *pane) clearOwnerForTest() {
7474 func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
7575 pty := newOwnerRoutingPTY()
7676 d, source, _, _ := newManualSessionWithPTYs(t, pty)
77- pane := source.activeTab().focusedPane()
77+ pane := testAttachmentTab(source).focusedPane()
7878 pane.onExit = func() {}
7979 source.snapEligible.Store(true)
8080
@@ -97,14 +97,14 @@ func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
9797 destinationCoordinator := d.attachCoordinator(destination, nil, destinationAttachment, true)
9898 destinationCoordinator.opts.onInvalidate = func(invalidation renderInvalidation) { destinationInvalidations <- invalidation }
9999
100- d.startPaneGoroutines(source, source.activeTab(), pane)
100+ d.startPaneGoroutines(source, testAttachmentTab(source), pane)
101101 pty.steps <- channelPTYStep{data: []byte("source\a")}
102102 <-pty.processed
103103 require.Len(t, sourceInvalidations, 1)
104104 require.Empty(t, destinationInvalidations)
105105 require.Equal(t, uint64(1), source.snapshotGeneration)
106106 require.Zero(t, destination.snapshotGeneration)
107- require.True(t, source.activeTab().attention)
107+ require.True(t, testAttachmentTab(source).attention)
108108
109109 installTiledPaneOwnerForTest(destination, destinationTab, pane)
110110 pty.steps <- channelPTYStep{data: []byte("destination\a")}
@@ -124,7 +124,7 @@ func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
124124 func TestPTYReaderExitReapsExactlyCurrentOwnerAfterPublication(t *testing.T) {
125125 pty := newOwnerRoutingPTY()
126126 d, source, _, _ := newManualSessionWithPTYs(t, pty)
127- sourceTab := source.activeTab()
127+ sourceTab := testAttachmentTab(source)
128128 moved := sourceTab.focusedPane()
129129
130130 destinationTab := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
@@ -74,7 +74,7 @@ func (p *pane) clearOwnerForTest() {
74 func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {74 func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
75 pty := newOwnerRoutingPTY()75 pty := newOwnerRoutingPTY()
76 d, source, _, _ := newManualSessionWithPTYs(t, pty)76 d, source, _, _ := newManualSessionWithPTYs(t, pty)
77- pane := source.activeTab().focusedPane()77+ pane := testAttachmentTab(source).focusedPane()
78 pane.onExit = func() {}78 pane.onExit = func() {}
79 source.snapEligible.Store(true)79 source.snapEligible.Store(true)
80 80
@@ -97,14 +97,14 @@ func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
97 destinationCoordinator := d.attachCoordinator(destination, nil, destinationAttachment, true)97 destinationCoordinator := d.attachCoordinator(destination, nil, destinationAttachment, true)
98 destinationCoordinator.opts.onInvalidate = func(invalidation renderInvalidation) { destinationInvalidations <- invalidation }98 destinationCoordinator.opts.onInvalidate = func(invalidation renderInvalidation) { destinationInvalidations <- invalidation }
99 99
100- d.startPaneGoroutines(source, source.activeTab(), pane)100+ d.startPaneGoroutines(source, testAttachmentTab(source), pane)
101 pty.steps <- channelPTYStep{data: []byte("source\a")}101 pty.steps <- channelPTYStep{data: []byte("source\a")}
102 <-pty.processed102 <-pty.processed
103 require.Len(t, sourceInvalidations, 1)103 require.Len(t, sourceInvalidations, 1)
104 require.Empty(t, destinationInvalidations)104 require.Empty(t, destinationInvalidations)
105 require.Equal(t, uint64(1), source.snapshotGeneration)105 require.Equal(t, uint64(1), source.snapshotGeneration)
106 require.Zero(t, destination.snapshotGeneration)106 require.Zero(t, destination.snapshotGeneration)
107- require.True(t, source.activeTab().attention)107+ require.True(t, testAttachmentTab(source).attention)
108 108
109 installTiledPaneOwnerForTest(destination, destinationTab, pane)109 installTiledPaneOwnerForTest(destination, destinationTab, pane)
110 pty.steps <- channelPTYStep{data: []byte("destination\a")}110 pty.steps <- channelPTYStep{data: []byte("destination\a")}
@@ -124,7 +124,7 @@ func TestPTYReaderRoutesEffectsToOwnerPublishedForEachRead(t *testing.T) {
124 func TestPTYReaderExitReapsExactlyCurrentOwnerAfterPublication(t *testing.T) {124 func TestPTYReaderExitReapsExactlyCurrentOwnerAfterPublication(t *testing.T) {
125 pty := newOwnerRoutingPTY()125 pty := newOwnerRoutingPTY()
126 d, source, _, _ := newManualSessionWithPTYs(t, pty)126 d, source, _, _ := newManualSessionWithPTYs(t, pty)
127- sourceTab := source.activeTab()127+ sourceTab := testAttachmentTab(source)
128 moved := sourceTab.focusedPane()128 moved := sourceTab.focusedPane()
129 129
130 destinationTab := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 23})130 destinationTab := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
M internal/usecase/daemon/pane_sync_owner_test.go +2 -2
@@ -27,8 +27,8 @@ func newPaneSyncOwnerFixture(t *testing.T) paneSyncOwnerFixture {
2727 size := domain.Size{Cols: 80, Rows: 23}
2828 sourceTab := newTab(newQuietPTY(), size)
2929 targetTab := newTab(newQuietPTY(), size)
30- source := &session{sessionCore: sessionCore{id: "sync-source", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{sourceTab}, active: 0}
31- destination := &session{sessionCore: sessionCore{id: "sync-destination", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{targetTab}, active: 0}
30+ source := &session{sessionCore: sessionCore{id: "sync-source", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{sourceTab}}
31+ destination := &session{sessionCore: sessionCore{id: "sync-destination", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{targetTab}}
3232 d := &Daemon{sessions: map[domain.SessionID]attachmentSession{source.id: source, destination.id: destination}}
3333 p := sourceTab.focusedPane()
3434 publishPaneOwner(p, source, sourceTab, 0)
@@ -27,8 +27,8 @@ func newPaneSyncOwnerFixture(t *testing.T) paneSyncOwnerFixture {
27 size := domain.Size{Cols: 80, Rows: 23}27 size := domain.Size{Cols: 80, Rows: 23}
28 sourceTab := newTab(newQuietPTY(), size)28 sourceTab := newTab(newQuietPTY(), size)
29 targetTab := newTab(newQuietPTY(), size)29 targetTab := newTab(newQuietPTY(), size)
30- source := &session{sessionCore: sessionCore{id: "sync-source", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{sourceTab}, active: 0}30+ source := &session{sessionCore: sessionCore{id: "sync-source", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{sourceTab}}
31- destination := &session{sessionCore: sessionCore{id: "sync-destination", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{targetTab}, active: 0}31+ destination := &session{sessionCore: sessionCore{id: "sync-destination", attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, tabs: []*tab{targetTab}}
32 d := &Daemon{sessions: map[domain.SessionID]attachmentSession{source.id: source, destination.id: destination}}32 d := &Daemon{sessions: map[domain.SessionID]attachmentSession{source.id: source, destination.id: destination}}
33 p := sourceTab.focusedPane()33 p := sourceTab.focusedPane()
34 publishPaneOwner(p, source, sourceTab, 0)34 publishPaneOwner(p, source, sourceTab, 0)
M internal/usecase/daemon/pane_titles.go +1 -1
@@ -151,7 +151,7 @@ func (d *Daemon) refreshPaneTitleCached(sess *session, id layout.PaneID, force b
151151 if sess == nil {
152152 return fallback
153153 }
154- tb := sess.tabForAttachmentOrActive(nil)
154+ tb := sess.firstTab()
155155 if tb == nil {
156156 return fallback
157157 }
@@ -151,7 +151,7 @@ func (d *Daemon) refreshPaneTitleCached(sess *session, id layout.PaneID, force b
151 if sess == nil {151 if sess == nil {
152 return fallback152 return fallback
153 }153 }
154- tb := sess.tabForAttachmentOrActive(nil)154+ tb := sess.firstTab()
155 if tb == nil {155 if tb == nil {
156 return fallback156 return fallback
157 }157 }
M internal/usecase/daemon/pane_titles_test.go +3 -3
@@ -78,7 +78,7 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) {
7878 title := d.refreshPaneTitle(sess, "pane-1")
7979 require.Equal(t, "vim", title)
8080
81- p := sess.activeTab().focusedPane()
81+ p := testAttachmentTab(sess).focusedPane()
8282 p.mu.Lock()
8383 p.screen.Write([]byte("\x1b]2;project/main.go\a"))
8484 p.refreshTerminalTitleLocked()
@@ -142,7 +142,7 @@ func TestRefreshFloatingPaneTitleUsesShellFallbackForEmptyCommand(t *testing.T)
142142
143143 func TestRefreshPaneTitleUpdatesNormalPaneShellFallback(t *testing.T) {
144144 d, sess, _, _ := newManualSessionWithPTYs(t, nil)
145- p := sess.activeTab().focusedPane()
145+ p := testAttachmentTab(sess).focusedPane()
146146
147147 d.shell = "/usr/bin/fish"
148148 require.Equal(t, "fish", d.refreshPaneTitle(sess, "pane-1"))
@@ -170,7 +170,7 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
170170 d := newTestDaemon(t, nil, clk)
171171 d.shell = "/usr/bin/fish"
172172 d.procComm = func(int) (string, error) { return "", errors.New("unused") }
173- p := sess.activeTab().focusedPane()
173+ p := testAttachmentTab(sess).focusedPane()
174174
175175 p.mu.Lock()
176176 p.screen.Write([]byte("\x1b]2;terminal\a"))
@@ -78,7 +78,7 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) {
78 title := d.refreshPaneTitle(sess, "pane-1")78 title := d.refreshPaneTitle(sess, "pane-1")
79 require.Equal(t, "vim", title)79 require.Equal(t, "vim", title)
80 80
81- p := sess.activeTab().focusedPane()81+ p := testAttachmentTab(sess).focusedPane()
82 p.mu.Lock()82 p.mu.Lock()
83 p.screen.Write([]byte("\x1b]2;project/main.go\a"))83 p.screen.Write([]byte("\x1b]2;project/main.go\a"))
84 p.refreshTerminalTitleLocked()84 p.refreshTerminalTitleLocked()
@@ -142,7 +142,7 @@ func TestRefreshFloatingPaneTitleUsesShellFallbackForEmptyCommand(t *testing.T)
142 142
143 func TestRefreshPaneTitleUpdatesNormalPaneShellFallback(t *testing.T) {143 func TestRefreshPaneTitleUpdatesNormalPaneShellFallback(t *testing.T) {
144 d, sess, _, _ := newManualSessionWithPTYs(t, nil)144 d, sess, _, _ := newManualSessionWithPTYs(t, nil)
145- p := sess.activeTab().focusedPane()145+ p := testAttachmentTab(sess).focusedPane()
146 146
147 d.shell = "/usr/bin/fish"147 d.shell = "/usr/bin/fish"
148 require.Equal(t, "fish", d.refreshPaneTitle(sess, "pane-1"))148 require.Equal(t, "fish", d.refreshPaneTitle(sess, "pane-1"))
@@ -170,7 +170,7 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
170 d := newTestDaemon(t, nil, clk)170 d := newTestDaemon(t, nil, clk)
171 d.shell = "/usr/bin/fish"171 d.shell = "/usr/bin/fish"
172 d.procComm = func(int) (string, error) { return "", errors.New("unused") }172 d.procComm = func(int) (string, error) { return "", errors.New("unused") }
173- p := sess.activeTab().focusedPane()173+ p := testAttachmentTab(sess).focusedPane()
174 174
175 p.mu.Lock()175 p.mu.Lock()
176 p.screen.Write([]byte("\x1b]2;terminal\a"))176 p.screen.Write([]byte("\x1b]2;terminal\a"))
M internal/usecase/daemon/performance_benchmark_test.go +1 -1
@@ -1565,7 +1565,7 @@ func performanceFullWidthRow(width, tabIndex, row int) string {
15651565
15661566 func (f *performanceFixture) findActivePane() *pane {
15671567 f.t.Helper()
1568- tb := f.sess.activeTab()
1568+ tb := testAttachmentTab(f.sess)
15691569 require.NotNil(f.t, tb)
15701570 tb.mu.Lock()
15711571 defer tb.mu.Unlock()
@@ -1565,7 +1565,7 @@ func performanceFullWidthRow(width, tabIndex, row int) string {
1565 1565
1566 func (f *performanceFixture) findActivePane() *pane {1566 func (f *performanceFixture) findActivePane() *pane {
1567 f.t.Helper()1567 f.t.Helper()
1568- tb := f.sess.activeTab()1568+ tb := testAttachmentTab(f.sess)
1569 require.NotNil(f.t, tb)1569 require.NotNil(f.t, tb)
1570 tb.mu.Lock()1570 tb.mu.Lock()
1571 defer tb.mu.Unlock()1571 defer tb.mu.Unlock()
M internal/usecase/daemon/picker.go +18 -8
@@ -52,7 +52,7 @@ func (d *Daemon) togglePickerSort() {
5252 // publishes its model, including an empty one; only move entry can fail for a
5353 // missing destination.
5454 func (d *Daemon) enterPicker(sess attachmentSession, ac *attachedClient) {
55- model := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
55+ model := d.newPickerModel(sess, ac, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
5656 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})
5757 }
5858
@@ -83,7 +83,7 @@ func (d *Daemon) publishPicker(sess attachmentSession, ac *attachedClient, model
8383
8484 // pickerViews captures one canonical lifecycle/tab snapshot. It intentionally
8585 // knows nothing about picker intent; picker.New owns all destination policy.
86-func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picker.SourceFilter) {
86+func (d *Daemon) pickerViews(cur attachmentSession, ac *attachedClient) ([]picker.SessionView, picker.SourceFilter) {
8787 d.mu.Lock()
8888 sessions := make([]attachmentSession, 0, len(d.sessions))
8989 proxies := make(map[domain.SessionID]*proxySession)
@@ -119,13 +119,23 @@ func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picke
119119 live := make([]liveSnapshot, 0, len(sessions))
120120 var current picker.SourceFilter
121121 for _, entry := range sessions {
122+ if entry == cur && ac != nil {
123+ if local, ok := localSession(entry); ok {
124+ local.repairAttachmentView(ac)
125+ }
126+ }
122127 snap := entry.snapshotView(opts)
123128 if entry == cur {
124129 if proxy, ok := entry.(*proxySession); ok {
125130 key := proxy.key
126131 current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, RemoteKey: &key}
127- } else if snap.active >= 0 && snap.active < len(snap.tabs) {
128- current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.active].id}
132+ } else if _, ok := localSession(entry); ok && ac != nil {
133+ view := ac.viewSnapshot()
134+ if view.tabID != "" {
135+ current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: view.tabID}
136+ }
137+ } else if len(snap.tabs) > 0 {
138+ current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.defaultTab].id}
129139 }
130140 }
131141 live = append(live, liveSnapshot{entry: entry, view: snap})
@@ -201,8 +211,8 @@ func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picke
201211 return views, current
202212 }
203213
204-func (d *Daemon) newPickerModel(cur attachmentSession, intent pickerIntent, source moveSourceLocator, current picker.SourceFilter) *picker.Model {
205- views, attachedCurrent := d.pickerViews(cur)
214+func (d *Daemon) newPickerModel(cur attachmentSession, ac *attachedClient, intent pickerIntent, source moveSourceLocator, current picker.SourceFilter) *picker.Model {
215+ views, attachedCurrent := d.pickerViews(cur, ac)
206216 if intent == pickerNavigate && current == (picker.SourceFilter{}) {
207217 current = attachedCurrent
208218 }
@@ -877,12 +887,12 @@ func (d *Daemon) switchToTargetGuardedForRole(from *session, ac *attachedClient,
877887 if sourceToken != nil {
878888 fresh, admitted := ac.beginRoleEffect(transition.published)
879889 if admitted {
880- d.activateTabAfterResizeForLease(from, from.tabForAttachmentOrActive(ac), false, ac, transition.published.lease)
890+ d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, transition.published.lease)
881891 d.invalidateRender(from, ac, true, "picker.go")
882892 fresh.End()
883893 }
884894 } else {
885- d.activateTabAfterResizeForLease(from, from.tabForAttachmentOrActive(ac), false, ac, nil)
895+ d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, nil)
886896 d.invalidateRender(from, ac, true, "picker.go")
887897 }
888898 } else {
@@ -52,7 +52,7 @@ func (d *Daemon) togglePickerSort() {
52 // publishes its model, including an empty one; only move entry can fail for a52 // publishes its model, including an empty one; only move entry can fail for a
53 // missing destination.53 // missing destination.
54 func (d *Daemon) enterPicker(sess attachmentSession, ac *attachedClient) {54 func (d *Daemon) enterPicker(sess attachmentSession, ac *attachedClient) {
55- model := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})55+ model := d.newPickerModel(sess, ac, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
56 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})56 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})
57 }57 }
58 58
@@ -83,7 +83,7 @@ func (d *Daemon) publishPicker(sess attachmentSession, ac *attachedClient, model
83 83
84 // pickerViews captures one canonical lifecycle/tab snapshot. It intentionally84 // pickerViews captures one canonical lifecycle/tab snapshot. It intentionally
85 // knows nothing about picker intent; picker.New owns all destination policy.85 // knows nothing about picker intent; picker.New owns all destination policy.
86-func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picker.SourceFilter) {86+func (d *Daemon) pickerViews(cur attachmentSession, ac *attachedClient) ([]picker.SessionView, picker.SourceFilter) {
87 d.mu.Lock()87 d.mu.Lock()
88 sessions := make([]attachmentSession, 0, len(d.sessions))88 sessions := make([]attachmentSession, 0, len(d.sessions))
89 proxies := make(map[domain.SessionID]*proxySession)89 proxies := make(map[domain.SessionID]*proxySession)
@@ -119,13 +119,23 @@ func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picke
119 live := make([]liveSnapshot, 0, len(sessions))119 live := make([]liveSnapshot, 0, len(sessions))
120 var current picker.SourceFilter120 var current picker.SourceFilter
121 for _, entry := range sessions {121 for _, entry := range sessions {
122+ if entry == cur && ac != nil {
123+ if local, ok := localSession(entry); ok {
124+ local.repairAttachmentView(ac)
125+ }
126+ }
122 snap := entry.snapshotView(opts)127 snap := entry.snapshotView(opts)
123 if entry == cur {128 if entry == cur {
124 if proxy, ok := entry.(*proxySession); ok {129 if proxy, ok := entry.(*proxySession); ok {
125 key := proxy.key130 key := proxy.key
126 current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, RemoteKey: &key}131 current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, RemoteKey: &key}
127- } else if snap.active >= 0 && snap.active < len(snap.tabs) {132+ } else if _, ok := localSession(entry); ok && ac != nil {
128- current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.active].id}133+ view := ac.viewSnapshot()
134+ if view.tabID != "" {
135+ current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: view.tabID}
136+ }
137+ } else if len(snap.tabs) > 0 {
138+ current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.defaultTab].id}
129 }139 }
130 }140 }
131 live = append(live, liveSnapshot{entry: entry, view: snap})141 live = append(live, liveSnapshot{entry: entry, view: snap})
@@ -201,8 +211,8 @@ func (d *Daemon) pickerViews(cur attachmentSession) ([]picker.SessionView, picke
201 return views, current211 return views, current
202 }212 }
203 213
204-func (d *Daemon) newPickerModel(cur attachmentSession, intent pickerIntent, source moveSourceLocator, current picker.SourceFilter) *picker.Model {214+func (d *Daemon) newPickerModel(cur attachmentSession, ac *attachedClient, intent pickerIntent, source moveSourceLocator, current picker.SourceFilter) *picker.Model {
205- views, attachedCurrent := d.pickerViews(cur)215+ views, attachedCurrent := d.pickerViews(cur, ac)
206 if intent == pickerNavigate && current == (picker.SourceFilter{}) {216 if intent == pickerNavigate && current == (picker.SourceFilter{}) {
207 current = attachedCurrent217 current = attachedCurrent
208 }218 }
@@ -877,12 +887,12 @@ func (d *Daemon) switchToTargetGuardedForRole(from *session, ac *attachedClient,
877 if sourceToken != nil {887 if sourceToken != nil {
878 fresh, admitted := ac.beginRoleEffect(transition.published)888 fresh, admitted := ac.beginRoleEffect(transition.published)
879 if admitted {889 if admitted {
880- d.activateTabAfterResizeForLease(from, from.tabForAttachmentOrActive(ac), false, ac, transition.published.lease)890+ d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, transition.published.lease)
881 d.invalidateRender(from, ac, true, "picker.go")891 d.invalidateRender(from, ac, true, "picker.go")
882 fresh.End()892 fresh.End()
883 }893 }
884 } else {894 } else {
885- d.activateTabAfterResizeForLease(from, from.tabForAttachmentOrActive(ac), false, ac, nil)895+ d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, nil)
886 d.invalidateRender(from, ac, true, "picker.go")896 d.invalidateRender(from, ac, true, "picker.go")
887 }897 }
888 } else {898 } else {
M internal/usecase/daemon/picker_test.go +23 -23
@@ -79,7 +79,7 @@ func TestPickerViewsAddsBellSuffixForAttention(t *testing.T) {
7979 d.sessions[current.id] = current
8080 d.sessions[ringing.id] = ringing
8181
82- views, currentSelection := d.pickerViews(current)
82+ views, currentSelection := d.pickerViews(current, nil)
8383
8484 require.Equal(t, picker.SourceFilter{Session: current.id}, currentSelection)
8585 require.Len(t, views, 2)
@@ -104,7 +104,7 @@ func TestPickerViewsIncludesEphemeralSessions(t *testing.T) {
104104 d.sessions[current.id] = current
105105 d.sessions[ephemeral.id] = ephemeral
106106
107- views, _ := d.pickerViews(current)
107+ views, _ := d.pickerViews(current, nil)
108108
109109 require.Len(t, views, 2)
110110 var ephemeralView picker.SessionView
@@ -132,7 +132,7 @@ func TestPickerViewsOrdersByMRUWithEphemeralInterleaved(t *testing.T) {
132132 d.stopped["halted-old"] = stoppedSession{name: "halted-old", createdAt: 10, lastUsedSeq: 1}
133133 d.stopped["halted-new"] = stoppedSession{name: "halted-new", createdAt: 11, lastUsedSeq: 2}
134134
135- views, _ := d.pickerViews(recent)
135+ views, _ := d.pickerViews(recent, nil)
136136
137137 names := make([]string, 0, len(views))
138138 for _, v := range views {
@@ -152,7 +152,7 @@ func TestPickerViewsMRUTieBreaksAlphabetically(t *testing.T) {
152152 d.sessions[b.id] = b
153153 d.sessions[a.id] = a
154154
155- views, _ := d.pickerViews(a)
155+ views, _ := d.pickerViews(a, nil)
156156
157157 require.Equal(t, "alpha", views[0].Name)
158158 require.Equal(t, "bravo", views[1].Name)
@@ -172,7 +172,7 @@ func TestPickerViewsGroupedModePutsNamedBeforeEphemeral(t *testing.T) {
172172 d.sessions[named2.id] = named2
173173 d.stopped["halted"] = stoppedSession{name: "halted", createdAt: 9, lastUsedSeq: 9}
174174
175- views, _ := d.pickerViews(named)
175+ views, _ := d.pickerViews(named, nil)
176176
177177 names := make([]string, 0, len(views))
178178 for _, v := range views {
@@ -283,7 +283,7 @@ func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
283283 d.sessions[active.id] = active
284284 d.stopped["stopped"] = stoppedSession{name: "stopped", createdAt: 24}
285285
286- views, _ := d.pickerViews(active)
286+ views, _ := d.pickerViews(active, nil)
287287
288288 require.Len(t, views, 2)
289289 require.NotNil(t, views[0].ExpectedCreatedAt)
@@ -294,7 +294,7 @@ func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
294294
295295 func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
296296 d := newTestDaemon(t, nil, stubClock{})
297- source := &session{sessionCore: sessionCore{id: "source", name: "source", incarnation: domain.IncarnationID{1}}, active: 0,
297+ source := &session{sessionCore: sessionCore{id: "source", name: "source", incarnation: domain.IncarnationID{1}},
298298 tabs: []*tab{
299299 {stableID: "source-tab", panes: map[layout.PaneID]*pane{}},
300300 {stableID: "sibling-tab", panes: map[layout.PaneID]*pane{}},
@@ -310,7 +310,7 @@ func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
310310 PaneID: "source-pane",
311311 }
312312
313- views, current := d.pickerViews(source)
313+ views, current := d.pickerViews(source, nil)
314314 require.Len(t, views, 3, "canonical capture includes active and stopped lifecycles")
315315 require.Equal(t, picker.SourceFilter{Session: source.id, Incarnation: source.incarnation, TabID: "source-tab"}, current)
316316 var sourceView picker.SessionView
@@ -322,14 +322,14 @@ func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
322322 require.Equal(t, []picker.TabEntry{{TabID: "source-tab", Name: "1"}, {TabID: "sibling-tab", Name: "2"}}, sourceView.Tabs,
323323 "daemon capture does not remove the move source")
324324
325- paneModel := d.newPickerModel(source, pickerMovePane, sourceLocator, picker.SourceFilter{
325+ paneModel := d.newPickerModel(source, nil, pickerMovePane, sourceLocator, picker.SourceFilter{
326326 Session: source.id, Incarnation: source.incarnation, TabID: "sibling-tab",
327327 })
328328 paneTarget, ok := paneModel.Selected()
329329 require.True(t, ok)
330330 require.Equal(t, domain.TabStableID("sibling-tab"), paneTarget.TabID)
331331
332- tabModel := d.newPickerModel(source, pickerMoveTab, sourceLocator, picker.SourceFilter{})
332+ tabModel := d.newPickerModel(source, nil, pickerMoveTab, sourceLocator, picker.SourceFilter{})
333333 tabTarget, ok := tabModel.Selected()
334334 require.True(t, ok)
335335 require.Equal(t, destination.id, tabTarget.Session)
@@ -445,7 +445,7 @@ func TestPickerMoveTabPreviewsDestinationActiveTabWithoutActivatingIt(t *testing
445445 sourceSession.id, sourceSession.name, sourceSession.incarnation = "source", "source", domain.IncarnationID{1}
446446 delete(d.sessions, domain.SessionID("manual"))
447447 d.sessions[sourceSession.id] = sourceSession
448- target := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, active: 1,
448+ target := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true},
449449 ctx: sourceSession.ctx, cancel: func() {},
450450 tabs: []*tab{
451451 newTestTabWithContext(p2, sourceSession.ctx, sourceSession.cancel),
@@ -471,7 +471,7 @@ func TestPickerMoveTabPreviewsDestinationActiveTabWithoutActivatingIt(t *testing
471471 require.Equal(t, target.name, selected.Name)
472472 require.Equal(t, -1, selected.TabIndex)
473473 require.Nil(t, selected.ExpectedCreatedAt)
474- require.Equal(t, 1, activeTabIndex(target), "preview must not mutate destination focus")
474+ require.Equal(t, 1, testAttachmentTabIndex(target), "preview must not mutate destination focus")
475475 d.closePicker(ac)
476476 }
477477
@@ -575,7 +575,7 @@ func TestPickerRejectsRecreatedEphemeralTargetFromStaleSelection(t *testing.T) {
575575 original := &session{sessionCore: sessionCore{id: "ephemeral-1", name: "2", ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{{}}}
576576 d.sessions[original.id] = original
577577
578- views, _ := d.pickerViews(current)
578+ views, _ := d.pickerViews(current, nil)
579579 var originalView picker.SessionView
580580 for _, view := range views {
581581 if view.ID == original.id {
@@ -604,7 +604,7 @@ func TestSameSessionSwitchRejectsReplacedClient(t *testing.T) {
604604 current.registerAttachment(&attachedClient{})
605605
606606 require.Error(t, d.switchToTarget(current, ac, picker.Target{Session: current.id, TabIndex: 1}))
607- require.Equal(t, 0, current.active)
607+ require.Equal(t, 0, testAttachmentTabIndex(current))
608608 }
609609
610610 func TestPickerViewsComposesFocusedPaneTitleWithAttentionSuffix(t *testing.T) {
@@ -626,7 +626,7 @@ func TestPickerViewsComposesFocusedPaneTitleWithAttentionSuffix(t *testing.T) {
626626 sess.tabs[1].attention = true
627627 sess.mu.Unlock()
628628
629- views, _ := d.pickerViews(sess)
629+ views, _ := d.pickerViews(sess, nil)
630630
631631 require.Len(t, views, 1)
632632 require.Equal(t, []picker.TabEntry{
@@ -653,7 +653,7 @@ func TestPickerViewsOmitsTerminalTitleWhenTabsConfigDisabled(t *testing.T) {
653653
654654 d.ApplyConfig(domain.Config{Tabs: domain.TabsConfig{TerminalTitle: false}})
655655
656- views, _ := d.pickerViews(sess)
656+ views, _ := d.pickerViews(sess, nil)
657657
658658 require.Len(t, views, 1)
659659 require.Equal(t, []picker.TabEntry{
@@ -844,8 +844,8 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
844844 awaitFrame(t, sends, ports.MsgOutput)
845845 d.handleInput(sess, ac, []byte("\r"))
846846
847- require.Equal(t, 1, activeTabIndex(sess))
848- requireFloatingInitialized(t, sess.activeTab())
847+ require.Equal(t, 1, testAttachmentTabIndex(sess))
848+ requireFloatingInitialized(t, testAttachmentTab(sess))
849849 awaitFrame(t, sends, ports.MsgOutput)
850850 d.enterPicker(sess, ac)
851851 awaitFrame(t, sends, ports.MsgOutput)
@@ -886,7 +886,7 @@ func TestPickerSplitArrowNavigatesWithoutExiting(t *testing.T) {
886886 }
887887 require.True(t, ac.overlays.pickerActive())
888888 d.handleInput(sess, ac, []byte("\r"))
889- require.Equal(t, tc.wantActive, activeTabIndex(sess))
889+ require.Equal(t, tc.wantActive, testAttachmentTabIndex(sess))
890890 })
891891 }
892892 }
@@ -1549,7 +1549,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
15491549 require.Contains(t, opens[1], "COLORTERM=truecolor")
15501550 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
15511551 require.Eventually(t, func() bool {
1552- tb := got.activeTab()
1552+ tb := testAttachmentTab(got)
15531553 if tb == nil {
15541554 return false
15551555 }
@@ -1640,7 +1640,8 @@ func TestPickerNavigationRefreshAfterDeleteSelectsReplacingRow(t *testing.T) {
16401640 t.Run(tt.name, func(t *testing.T) {
16411641 d, current, ac, _, currentReleases := newManualTabSession(t, 2)
16421642 defer releaseAll(currentReleases)
1643- current.id, current.name, current.active = "current", tt.currentName, 1
1643+ current.id, current.name = "current", tt.currentName
1644+ selectTestAttachmentTab(current, 1)
16441645 current.tabs[0].stableID, current.tabs[1].stableID = "current-first", "current-active"
16451646 delete(d.sessions, domain.SessionID("manual"))
16461647 d.sessions[current.id] = current
@@ -1811,8 +1812,7 @@ func TestPickerKillStoppedSessionPersistDeleteFailureSurfacesNoticeAndKeepsEntry
18111812 require.True(t, stopped.purging, "an uncertain deletion stays fenced from restore")
18121813
18131814 from.mu.Lock()
1814- activeTab := from.tabs[from.active]
1815- activeTabID := activeTab.stableID
1815+ activeTabID := testAttachmentTabLocked(from).stableID
18161816 from.mu.Unlock()
18171817 ac.overlays.pickerMu.Lock()
18181818 selected, selectedOK := ac.overlays.picker.Selected()
@@ -79,7 +79,7 @@ func TestPickerViewsAddsBellSuffixForAttention(t *testing.T) {
79 d.sessions[current.id] = current79 d.sessions[current.id] = current
80 d.sessions[ringing.id] = ringing80 d.sessions[ringing.id] = ringing
81 81
82- views, currentSelection := d.pickerViews(current)82+ views, currentSelection := d.pickerViews(current, nil)
83 83
84 require.Equal(t, picker.SourceFilter{Session: current.id}, currentSelection)84 require.Equal(t, picker.SourceFilter{Session: current.id}, currentSelection)
85 require.Len(t, views, 2)85 require.Len(t, views, 2)
@@ -104,7 +104,7 @@ func TestPickerViewsIncludesEphemeralSessions(t *testing.T) {
104 d.sessions[current.id] = current104 d.sessions[current.id] = current
105 d.sessions[ephemeral.id] = ephemeral105 d.sessions[ephemeral.id] = ephemeral
106 106
107- views, _ := d.pickerViews(current)107+ views, _ := d.pickerViews(current, nil)
108 108
109 require.Len(t, views, 2)109 require.Len(t, views, 2)
110 var ephemeralView picker.SessionView110 var ephemeralView picker.SessionView
@@ -132,7 +132,7 @@ func TestPickerViewsOrdersByMRUWithEphemeralInterleaved(t *testing.T) {
132 d.stopped["halted-old"] = stoppedSession{name: "halted-old", createdAt: 10, lastUsedSeq: 1}132 d.stopped["halted-old"] = stoppedSession{name: "halted-old", createdAt: 10, lastUsedSeq: 1}
133 d.stopped["halted-new"] = stoppedSession{name: "halted-new", createdAt: 11, lastUsedSeq: 2}133 d.stopped["halted-new"] = stoppedSession{name: "halted-new", createdAt: 11, lastUsedSeq: 2}
134 134
135- views, _ := d.pickerViews(recent)135+ views, _ := d.pickerViews(recent, nil)
136 136
137 names := make([]string, 0, len(views))137 names := make([]string, 0, len(views))
138 for _, v := range views {138 for _, v := range views {
@@ -152,7 +152,7 @@ func TestPickerViewsMRUTieBreaksAlphabetically(t *testing.T) {
152 d.sessions[b.id] = b152 d.sessions[b.id] = b
153 d.sessions[a.id] = a153 d.sessions[a.id] = a
154 154
155- views, _ := d.pickerViews(a)155+ views, _ := d.pickerViews(a, nil)
156 156
157 require.Equal(t, "alpha", views[0].Name)157 require.Equal(t, "alpha", views[0].Name)
158 require.Equal(t, "bravo", views[1].Name)158 require.Equal(t, "bravo", views[1].Name)
@@ -172,7 +172,7 @@ func TestPickerViewsGroupedModePutsNamedBeforeEphemeral(t *testing.T) {
172 d.sessions[named2.id] = named2172 d.sessions[named2.id] = named2
173 d.stopped["halted"] = stoppedSession{name: "halted", createdAt: 9, lastUsedSeq: 9}173 d.stopped["halted"] = stoppedSession{name: "halted", createdAt: 9, lastUsedSeq: 9}
174 174
175- views, _ := d.pickerViews(named)175+ views, _ := d.pickerViews(named, nil)
176 176
177 names := make([]string, 0, len(views))177 names := make([]string, 0, len(views))
178 for _, v := range views {178 for _, v := range views {
@@ -283,7 +283,7 @@ func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
283 d.sessions[active.id] = active283 d.sessions[active.id] = active
284 d.stopped["stopped"] = stoppedSession{name: "stopped", createdAt: 24}284 d.stopped["stopped"] = stoppedSession{name: "stopped", createdAt: 24}
285 285
286- views, _ := d.pickerViews(active)286+ views, _ := d.pickerViews(active, nil)
287 287
288 require.Len(t, views, 2)288 require.Len(t, views, 2)
289 require.NotNil(t, views[0].ExpectedCreatedAt)289 require.NotNil(t, views[0].ExpectedCreatedAt)
@@ -294,7 +294,7 @@ func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
294 294
295 func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {295 func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
296 d := newTestDaemon(t, nil, stubClock{})296 d := newTestDaemon(t, nil, stubClock{})
297- source := &session{sessionCore: sessionCore{id: "source", name: "source", incarnation: domain.IncarnationID{1}}, active: 0,297+ source := &session{sessionCore: sessionCore{id: "source", name: "source", incarnation: domain.IncarnationID{1}},
298 tabs: []*tab{298 tabs: []*tab{
299 {stableID: "source-tab", panes: map[layout.PaneID]*pane{}},299 {stableID: "source-tab", panes: map[layout.PaneID]*pane{}},
300 {stableID: "sibling-tab", panes: map[layout.PaneID]*pane{}},300 {stableID: "sibling-tab", panes: map[layout.PaneID]*pane{}},
@@ -310,7 +310,7 @@ func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
310 PaneID: "source-pane",310 PaneID: "source-pane",
311 }311 }
312 312
313- views, current := d.pickerViews(source)313+ views, current := d.pickerViews(source, nil)
314 require.Len(t, views, 3, "canonical capture includes active and stopped lifecycles")314 require.Len(t, views, 3, "canonical capture includes active and stopped lifecycles")
315 require.Equal(t, picker.SourceFilter{Session: source.id, Incarnation: source.incarnation, TabID: "source-tab"}, current)315 require.Equal(t, picker.SourceFilter{Session: source.id, Incarnation: source.incarnation, TabID: "source-tab"}, current)
316 var sourceView picker.SessionView316 var sourceView picker.SessionView
@@ -322,14 +322,14 @@ func TestPickerCanonicalViewsLeaveMoveFilteringToModel(t *testing.T) {
322 require.Equal(t, []picker.TabEntry{{TabID: "source-tab", Name: "1"}, {TabID: "sibling-tab", Name: "2"}}, sourceView.Tabs,322 require.Equal(t, []picker.TabEntry{{TabID: "source-tab", Name: "1"}, {TabID: "sibling-tab", Name: "2"}}, sourceView.Tabs,
323 "daemon capture does not remove the move source")323 "daemon capture does not remove the move source")
324 324
325- paneModel := d.newPickerModel(source, pickerMovePane, sourceLocator, picker.SourceFilter{325+ paneModel := d.newPickerModel(source, nil, pickerMovePane, sourceLocator, picker.SourceFilter{
326 Session: source.id, Incarnation: source.incarnation, TabID: "sibling-tab",326 Session: source.id, Incarnation: source.incarnation, TabID: "sibling-tab",
327 })327 })
328 paneTarget, ok := paneModel.Selected()328 paneTarget, ok := paneModel.Selected()
329 require.True(t, ok)329 require.True(t, ok)
330 require.Equal(t, domain.TabStableID("sibling-tab"), paneTarget.TabID)330 require.Equal(t, domain.TabStableID("sibling-tab"), paneTarget.TabID)
331 331
332- tabModel := d.newPickerModel(source, pickerMoveTab, sourceLocator, picker.SourceFilter{})332+ tabModel := d.newPickerModel(source, nil, pickerMoveTab, sourceLocator, picker.SourceFilter{})
333 tabTarget, ok := tabModel.Selected()333 tabTarget, ok := tabModel.Selected()
334 require.True(t, ok)334 require.True(t, ok)
335 require.Equal(t, destination.id, tabTarget.Session)335 require.Equal(t, destination.id, tabTarget.Session)
@@ -445,7 +445,7 @@ func TestPickerMoveTabPreviewsDestinationActiveTabWithoutActivatingIt(t *testing
445 sourceSession.id, sourceSession.name, sourceSession.incarnation = "source", "source", domain.IncarnationID{1}445 sourceSession.id, sourceSession.name, sourceSession.incarnation = "source", "source", domain.IncarnationID{1}
446 delete(d.sessions, domain.SessionID("manual"))446 delete(d.sessions, domain.SessionID("manual"))
447 d.sessions[sourceSession.id] = sourceSession447 d.sessions[sourceSession.id] = sourceSession
448- target := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true}, active: 1,448+ target := &session{sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{2}, ephemeral: true},
449 ctx: sourceSession.ctx, cancel: func() {},449 ctx: sourceSession.ctx, cancel: func() {},
450 tabs: []*tab{450 tabs: []*tab{
451 newTestTabWithContext(p2, sourceSession.ctx, sourceSession.cancel),451 newTestTabWithContext(p2, sourceSession.ctx, sourceSession.cancel),
@@ -471,7 +471,7 @@ func TestPickerMoveTabPreviewsDestinationActiveTabWithoutActivatingIt(t *testing
471 require.Equal(t, target.name, selected.Name)471 require.Equal(t, target.name, selected.Name)
472 require.Equal(t, -1, selected.TabIndex)472 require.Equal(t, -1, selected.TabIndex)
473 require.Nil(t, selected.ExpectedCreatedAt)473 require.Nil(t, selected.ExpectedCreatedAt)
474- require.Equal(t, 1, activeTabIndex(target), "preview must not mutate destination focus")474+ require.Equal(t, 1, testAttachmentTabIndex(target), "preview must not mutate destination focus")
475 d.closePicker(ac)475 d.closePicker(ac)
476 }476 }
477 477
@@ -575,7 +575,7 @@ func TestPickerRejectsRecreatedEphemeralTargetFromStaleSelection(t *testing.T) {
575 original := &session{sessionCore: sessionCore{id: "ephemeral-1", name: "2", ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{{}}}575 original := &session{sessionCore: sessionCore{id: "ephemeral-1", name: "2", ephemeral: true}, ctx: ctx, cancel: cancel, tabs: []*tab{{}}}
576 d.sessions[original.id] = original576 d.sessions[original.id] = original
577 577
578- views, _ := d.pickerViews(current)578+ views, _ := d.pickerViews(current, nil)
579 var originalView picker.SessionView579 var originalView picker.SessionView
580 for _, view := range views {580 for _, view := range views {
581 if view.ID == original.id {581 if view.ID == original.id {
@@ -604,7 +604,7 @@ func TestSameSessionSwitchRejectsReplacedClient(t *testing.T) {
604 current.registerAttachment(&attachedClient{})604 current.registerAttachment(&attachedClient{})
605 605
606 require.Error(t, d.switchToTarget(current, ac, picker.Target{Session: current.id, TabIndex: 1}))606 require.Error(t, d.switchToTarget(current, ac, picker.Target{Session: current.id, TabIndex: 1}))
607- require.Equal(t, 0, current.active)607+ require.Equal(t, 0, testAttachmentTabIndex(current))
608 }608 }
609 609
610 func TestPickerViewsComposesFocusedPaneTitleWithAttentionSuffix(t *testing.T) {610 func TestPickerViewsComposesFocusedPaneTitleWithAttentionSuffix(t *testing.T) {
@@ -626,7 +626,7 @@ func TestPickerViewsComposesFocusedPaneTitleWithAttentionSuffix(t *testing.T) {
626 sess.tabs[1].attention = true626 sess.tabs[1].attention = true
627 sess.mu.Unlock()627 sess.mu.Unlock()
628 628
629- views, _ := d.pickerViews(sess)629+ views, _ := d.pickerViews(sess, nil)
630 630
631 require.Len(t, views, 1)631 require.Len(t, views, 1)
632 require.Equal(t, []picker.TabEntry{632 require.Equal(t, []picker.TabEntry{
@@ -653,7 +653,7 @@ func TestPickerViewsOmitsTerminalTitleWhenTabsConfigDisabled(t *testing.T) {
653 653
654 d.ApplyConfig(domain.Config{Tabs: domain.TabsConfig{TerminalTitle: false}})654 d.ApplyConfig(domain.Config{Tabs: domain.TabsConfig{TerminalTitle: false}})
655 655
656- views, _ := d.pickerViews(sess)656+ views, _ := d.pickerViews(sess, nil)
657 657
658 require.Len(t, views, 1)658 require.Len(t, views, 1)
659 require.Equal(t, []picker.TabEntry{659 require.Equal(t, []picker.TabEntry{
@@ -844,8 +844,8 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
844 awaitFrame(t, sends, ports.MsgOutput)844 awaitFrame(t, sends, ports.MsgOutput)
845 d.handleInput(sess, ac, []byte("\r"))845 d.handleInput(sess, ac, []byte("\r"))
846 846
847- require.Equal(t, 1, activeTabIndex(sess))847+ require.Equal(t, 1, testAttachmentTabIndex(sess))
848- requireFloatingInitialized(t, sess.activeTab())848+ requireFloatingInitialized(t, testAttachmentTab(sess))
849 awaitFrame(t, sends, ports.MsgOutput)849 awaitFrame(t, sends, ports.MsgOutput)
850 d.enterPicker(sess, ac)850 d.enterPicker(sess, ac)
851 awaitFrame(t, sends, ports.MsgOutput)851 awaitFrame(t, sends, ports.MsgOutput)
@@ -886,7 +886,7 @@ func TestPickerSplitArrowNavigatesWithoutExiting(t *testing.T) {
886 }886 }
887 require.True(t, ac.overlays.pickerActive())887 require.True(t, ac.overlays.pickerActive())
888 d.handleInput(sess, ac, []byte("\r"))888 d.handleInput(sess, ac, []byte("\r"))
889- require.Equal(t, tc.wantActive, activeTabIndex(sess))889+ require.Equal(t, tc.wantActive, testAttachmentTabIndex(sess))
890 })890 })
891 }891 }
892 }892 }
@@ -1549,7 +1549,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
1549 require.Contains(t, opens[1], "COLORTERM=truecolor")1549 require.Contains(t, opens[1], "COLORTERM=truecolor")
1550 require.Contains(t, opens[1], "TERM_PROGRAM=vev")1550 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
1551 require.Eventually(t, func() bool {1551 require.Eventually(t, func() bool {
1552- tb := got.activeTab()1552+ tb := testAttachmentTab(got)
1553 if tb == nil {1553 if tb == nil {
1554 return false1554 return false
1555 }1555 }
@@ -1640,7 +1640,8 @@ func TestPickerNavigationRefreshAfterDeleteSelectsReplacingRow(t *testing.T) {
1640 t.Run(tt.name, func(t *testing.T) {1640 t.Run(tt.name, func(t *testing.T) {
1641 d, current, ac, _, currentReleases := newManualTabSession(t, 2)1641 d, current, ac, _, currentReleases := newManualTabSession(t, 2)
1642 defer releaseAll(currentReleases)1642 defer releaseAll(currentReleases)
1643- current.id, current.name, current.active = "current", tt.currentName, 11643+ current.id, current.name = "current", tt.currentName
1644+ selectTestAttachmentTab(current, 1)
1644 current.tabs[0].stableID, current.tabs[1].stableID = "current-first", "current-active"1645 current.tabs[0].stableID, current.tabs[1].stableID = "current-first", "current-active"
1645 delete(d.sessions, domain.SessionID("manual"))1646 delete(d.sessions, domain.SessionID("manual"))
1646 d.sessions[current.id] = current1647 d.sessions[current.id] = current
@@ -1811,8 +1812,7 @@ func TestPickerKillStoppedSessionPersistDeleteFailureSurfacesNoticeAndKeepsEntry
1811 require.True(t, stopped.purging, "an uncertain deletion stays fenced from restore")1812 require.True(t, stopped.purging, "an uncertain deletion stays fenced from restore")
1812 1813
1813 from.mu.Lock()1814 from.mu.Lock()
1814- activeTab := from.tabs[from.active]1815+ activeTabID := testAttachmentTabLocked(from).stableID
1815- activeTabID := activeTab.stableID
1816 from.mu.Unlock()1816 from.mu.Unlock()
1817 ac.overlays.pickerMu.Lock()1817 ac.overlays.pickerMu.Lock()
1818 selected, selectedOK := ac.overlays.picker.Selected()1818 selected, selectedOK := ac.overlays.picker.Selected()
M internal/usecase/daemon/proxied_contract_test.go +8 -8
@@ -57,9 +57,9 @@ func TestProxiedHandshakeOrdersWelcomeMetadataAndBaseZeroContent(t *testing.T) {
5757
5858 sess := firstSession(d)
5959 require.NotNil(t, sess)
60- sess.activeTab().mu.Lock()
61- actualContentSize := sess.activeTab().size
62- sess.activeTab().mu.Unlock()
60+ testAttachmentTab(sess).mu.Lock()
61+ actualContentSize := testAttachmentTab(sess).size
62+ testAttachmentTab(sess).mu.Unlock()
6363 require.Equal(t, contentSize, actualContentSize, "proxied geometry must not subtract chrome a second time")
6464
6565 release()
@@ -94,9 +94,9 @@ func TestOrdinaryHandshakeRetainsChromeAndNoMetadata(t *testing.T) {
9494
9595 sess := firstSession(d)
9696 require.NotNil(t, sess)
97- sess.activeTab().mu.Lock()
98- actualContentSize := sess.activeTab().size
99- sess.activeTab().mu.Unlock()
97+ testAttachmentTab(sess).mu.Lock()
98+ actualContentSize := testAttachmentTab(sess).size
99+ testAttachmentTab(sess).mu.Unlock()
100100 require.Equal(t, tabSize(viewport), actualContentSize)
101101 requireNoOutputFrame(t, sends)
102102
@@ -143,7 +143,7 @@ func TestProxiedRemotePaintSendsMetadataBeforeScreen(t *testing.T) {
143143 func TestProxiedPaintPublishesAbsoluteVisibleCursorStyle(t *testing.T) {
144144 d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY())
145145 ac.proxied = true
146- pane := sess.activeTab().focusedPane()
146+ pane := testAttachmentTab(sess).focusedPane()
147147 pane.mu.Lock()
148148 pane.screen.Write([]byte("\x1b[3;6H\x1b[0 q"))
149149 pane.mu.Unlock()
@@ -209,7 +209,7 @@ func TestProxiedTransactionalResizeUsesReceivedContentGeometry(t *testing.T) {
209209
210210 want := domain.Size{Cols: 100, Rows: 12}
211211 require.True(t, d.requestTransactionalResizeForLease(sess, ac, lease, want, true))
212- tb := sess.activeTab()
212+ tb := testAttachmentTab(sess)
213213 tb.mu.Lock()
214214 require.Equal(t, want, tb.size)
215215 tb.mu.Unlock()
@@ -57,9 +57,9 @@ func TestProxiedHandshakeOrdersWelcomeMetadataAndBaseZeroContent(t *testing.T) {
57 57
58 sess := firstSession(d)58 sess := firstSession(d)
59 require.NotNil(t, sess)59 require.NotNil(t, sess)
60- sess.activeTab().mu.Lock()60+ testAttachmentTab(sess).mu.Lock()
61- actualContentSize := sess.activeTab().size61+ actualContentSize := testAttachmentTab(sess).size
62- sess.activeTab().mu.Unlock()62+ testAttachmentTab(sess).mu.Unlock()
63 require.Equal(t, contentSize, actualContentSize, "proxied geometry must not subtract chrome a second time")63 require.Equal(t, contentSize, actualContentSize, "proxied geometry must not subtract chrome a second time")
64 64
65 release()65 release()
@@ -94,9 +94,9 @@ func TestOrdinaryHandshakeRetainsChromeAndNoMetadata(t *testing.T) {
94 94
95 sess := firstSession(d)95 sess := firstSession(d)
96 require.NotNil(t, sess)96 require.NotNil(t, sess)
97- sess.activeTab().mu.Lock()97+ testAttachmentTab(sess).mu.Lock()
98- actualContentSize := sess.activeTab().size98+ actualContentSize := testAttachmentTab(sess).size
99- sess.activeTab().mu.Unlock()99+ testAttachmentTab(sess).mu.Unlock()
100 require.Equal(t, tabSize(viewport), actualContentSize)100 require.Equal(t, tabSize(viewport), actualContentSize)
101 requireNoOutputFrame(t, sends)101 requireNoOutputFrame(t, sends)
102 102
@@ -143,7 +143,7 @@ func TestProxiedRemotePaintSendsMetadataBeforeScreen(t *testing.T) {
143 func TestProxiedPaintPublishesAbsoluteVisibleCursorStyle(t *testing.T) {143 func TestProxiedPaintPublishesAbsoluteVisibleCursorStyle(t *testing.T) {
144 d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY())144 d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY())
145 ac.proxied = true145 ac.proxied = true
146- pane := sess.activeTab().focusedPane()146+ pane := testAttachmentTab(sess).focusedPane()
147 pane.mu.Lock()147 pane.mu.Lock()
148 pane.screen.Write([]byte("\x1b[3;6H\x1b[0 q"))148 pane.screen.Write([]byte("\x1b[3;6H\x1b[0 q"))
149 pane.mu.Unlock()149 pane.mu.Unlock()
@@ -209,7 +209,7 @@ func TestProxiedTransactionalResizeUsesReceivedContentGeometry(t *testing.T) {
209 209
210 want := domain.Size{Cols: 100, Rows: 12}210 want := domain.Size{Cols: 100, Rows: 12}
211 require.True(t, d.requestTransactionalResizeForLease(sess, ac, lease, want, true))211 require.True(t, d.requestTransactionalResizeForLease(sess, ac, lease, want, true))
212- tb := sess.activeTab()212+ tb := testAttachmentTab(sess)
213 tb.mu.Lock()213 tb.mu.Lock()
214 require.Equal(t, want, tb.size)214 require.Equal(t, want, tb.size)
215 tb.mu.Unlock()215 tb.mu.Unlock()
M internal/usecase/daemon/proxy_input_test.go +9 -9
@@ -514,7 +514,7 @@ func TestProxyAttachedCommandRejectsTargetsAndExecutesExactActiveSession(t *test
514514 publishTiledPaneOwners(sess, second)
515515 sess.mu.Lock()
516516 sess.tabs = append(sess.tabs, second)
517- sess.active = 0
517+ selectTestAttachmentTabLocked(sess, 0)
518518 sess.mu.Unlock()
519519 d.attachCoordinator(sess, nil, ac, true)
520520
@@ -532,13 +532,13 @@ func TestProxyAttachedCommandRejectsTargetsAndExecutesExactActiveSession(t *test
532532 result := send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 1, Attached: true, Slug: "next-tab"})
533533 require.True(t, result.OK)
534534 sess.mu.Lock()
535- require.Equal(t, 1, sess.active)
535+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess))
536536 sess.mu.Unlock()
537537
538538 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 2, Attached: true, Slug: "previous-tab", TargetSession: "other"})
539539 require.False(t, result.OK)
540540 sess.mu.Lock()
541- require.Equal(t, 1, sess.active, "target override must not execute against any session")
541+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess), "target override must not execute against any session")
542542 sess.mu.Unlock()
543543
544544 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 3, Attached: true, Slug: "rename-tab"})
@@ -669,7 +669,7 @@ func TestRemotePickerEnterRoutesStructuredKeyThroughProxyOwnership(t *testing.T)
669669 Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},
670670 }})
671671
672- model := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
672+ model := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
673673 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})
674674 target, selectable := model.Selected()
675675 require.True(t, selectable)
@@ -694,7 +694,7 @@ func TestRemotePickerSwitchesFromProxyBackToLocalSession(t *testing.T) {
694694 require.True(t, d.registerSessionLocked(local))
695695 d.mu.Unlock()
696696
697- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
697+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
698698 d.publishPicker(proxy, ac, model, pickerNavigate, moveSourceLocator{})
699699 target, selectable := model.Selected()
700700 require.True(t, selectable)
@@ -734,7 +734,7 @@ func TestRemotePickerRejectsReplacedLocalLifecycle(t *testing.T) {
734734 require.True(t, d.registerSessionLocked(local))
735735 d.mu.Unlock()
736736
737- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
737+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
738738 target, selectable := model.Selected()
739739 require.True(t, selectable)
740740 d.afterRoleEffectsFrozen = func() {
@@ -759,7 +759,7 @@ func TestRemotePickerRejectsReplacedLocalIncarnation(t *testing.T) {
759759 require.True(t, d.registerSessionLocked(local))
760760 d.mu.Unlock()
761761
762- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, Incarnation: local.incarnation, TabID: domain.TabStableID(tb.stableID)})
762+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, Incarnation: local.incarnation, TabID: domain.TabStableID(tb.stableID)})
763763 target, selectable := model.Selected()
764764 require.True(t, selectable)
765765 d.afterRoleEffectsFrozen = func() {
@@ -786,7 +786,7 @@ func TestRemotePickerRejectsReplacedLocalTab(t *testing.T) {
786786 require.True(t, d.registerSessionLocked(local))
787787 d.mu.Unlock()
788788
789- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(second.stableID)})
789+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(second.stableID)})
790790 target, selectable := model.Selected()
791791 require.True(t, selectable)
792792 require.Equal(t, domain.TabStableID(second.stableID), target.TabID)
@@ -822,7 +822,7 @@ func TestRemotePickerCanReselectWarmProxyAfterReturningLocal(t *testing.T) {
822822 require.NoError(t, d.switchToTargetForRole(proxyToken, picker.Target{Session: local.id, TabIndex: 0}, sessionHandoffGuard{}, "picker-select"))
823823 require.Same(t, local, ac.currentSession())
824824
825- model := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
825+ model := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
826826 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})
827827 target, selectable := model.Selected()
828828 require.True(t, selectable)
@@ -514,7 +514,7 @@ func TestProxyAttachedCommandRejectsTargetsAndExecutesExactActiveSession(t *test
514 publishTiledPaneOwners(sess, second)514 publishTiledPaneOwners(sess, second)
515 sess.mu.Lock()515 sess.mu.Lock()
516 sess.tabs = append(sess.tabs, second)516 sess.tabs = append(sess.tabs, second)
517- sess.active = 0517+ selectTestAttachmentTabLocked(sess, 0)
518 sess.mu.Unlock()518 sess.mu.Unlock()
519 d.attachCoordinator(sess, nil, ac, true)519 d.attachCoordinator(sess, nil, ac, true)
520 520
@@ -532,13 +532,13 @@ func TestProxyAttachedCommandRejectsTargetsAndExecutesExactActiveSession(t *test
532 result := send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 1, Attached: true, Slug: "next-tab"})532 result := send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 1, Attached: true, Slug: "next-tab"})
533 require.True(t, result.OK)533 require.True(t, result.OK)
534 sess.mu.Lock()534 sess.mu.Lock()
535- require.Equal(t, 1, sess.active)535+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess))
536 sess.mu.Unlock()536 sess.mu.Unlock()
537 537
538 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 2, Attached: true, Slug: "previous-tab", TargetSession: "other"})538 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 2, Attached: true, Slug: "previous-tab", TargetSession: "other"})
539 require.False(t, result.OK)539 require.False(t, result.OK)
540 sess.mu.Lock()540 sess.mu.Lock()
541- require.Equal(t, 1, sess.active, "target override must not execute against any session")541+ require.Equal(t, 1, testAttachmentTabIndexLocked(sess), "target override must not execute against any session")
542 sess.mu.Unlock()542 sess.mu.Unlock()
543 543
544 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 3, Attached: true, Slug: "rename-tab"})544 result = send(ports.CommandRequest{Version: ports.ProtocolVersion, RequestID: 3, Attached: true, Slug: "rename-tab"})
@@ -669,7 +669,7 @@ func TestRemotePickerEnterRoutesStructuredKeyThroughProxyOwnership(t *testing.T)
669 Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},669 Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},
670 }})670 }})
671 671
672- model := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})672+ model := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
673 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})673 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})
674 target, selectable := model.Selected()674 target, selectable := model.Selected()
675 require.True(t, selectable)675 require.True(t, selectable)
@@ -694,7 +694,7 @@ func TestRemotePickerSwitchesFromProxyBackToLocalSession(t *testing.T) {
694 require.True(t, d.registerSessionLocked(local))694 require.True(t, d.registerSessionLocked(local))
695 d.mu.Unlock()695 d.mu.Unlock()
696 696
697- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})697+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
698 d.publishPicker(proxy, ac, model, pickerNavigate, moveSourceLocator{})698 d.publishPicker(proxy, ac, model, pickerNavigate, moveSourceLocator{})
699 target, selectable := model.Selected()699 target, selectable := model.Selected()
700 require.True(t, selectable)700 require.True(t, selectable)
@@ -734,7 +734,7 @@ func TestRemotePickerRejectsReplacedLocalLifecycle(t *testing.T) {
734 require.True(t, d.registerSessionLocked(local))734 require.True(t, d.registerSessionLocked(local))
735 d.mu.Unlock()735 d.mu.Unlock()
736 736
737- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})737+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(tb.stableID)})
738 target, selectable := model.Selected()738 target, selectable := model.Selected()
739 require.True(t, selectable)739 require.True(t, selectable)
740 d.afterRoleEffectsFrozen = func() {740 d.afterRoleEffectsFrozen = func() {
@@ -759,7 +759,7 @@ func TestRemotePickerRejectsReplacedLocalIncarnation(t *testing.T) {
759 require.True(t, d.registerSessionLocked(local))759 require.True(t, d.registerSessionLocked(local))
760 d.mu.Unlock()760 d.mu.Unlock()
761 761
762- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, Incarnation: local.incarnation, TabID: domain.TabStableID(tb.stableID)})762+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, Incarnation: local.incarnation, TabID: domain.TabStableID(tb.stableID)})
763 target, selectable := model.Selected()763 target, selectable := model.Selected()
764 require.True(t, selectable)764 require.True(t, selectable)
765 d.afterRoleEffectsFrozen = func() {765 d.afterRoleEffectsFrozen = func() {
@@ -786,7 +786,7 @@ func TestRemotePickerRejectsReplacedLocalTab(t *testing.T) {
786 require.True(t, d.registerSessionLocked(local))786 require.True(t, d.registerSessionLocked(local))
787 d.mu.Unlock()787 d.mu.Unlock()
788 788
789- model := d.newPickerModel(proxy, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(second.stableID)})789+ model := d.newPickerModel(proxy, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: local.id, TabID: domain.TabStableID(second.stableID)})
790 target, selectable := model.Selected()790 target, selectable := model.Selected()
791 require.True(t, selectable)791 require.True(t, selectable)
792 require.Equal(t, domain.TabStableID(second.stableID), target.TabID)792 require.Equal(t, domain.TabStableID(second.stableID), target.TabID)
@@ -822,7 +822,7 @@ func TestRemotePickerCanReselectWarmProxyAfterReturningLocal(t *testing.T) {
822 require.NoError(t, d.switchToTargetForRole(proxyToken, picker.Target{Session: local.id, TabIndex: 0}, sessionHandoffGuard{}, "picker-select"))822 require.NoError(t, d.switchToTargetForRole(proxyToken, picker.Target{Session: local.id, TabIndex: 0}, sessionHandoffGuard{}, "picker-select"))
823 require.Same(t, local, ac.currentSession())823 require.Same(t, local, ac.currentSession())
824 824
825- model := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})825+ model := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
826 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})826 d.publishPicker(local, ac, model, pickerNavigate, moveSourceLocator{})
827 target, selectable := model.Selected()827 target, selectable := model.Selected()
828 require.True(t, selectable)828 require.True(t, selectable)
M internal/usecase/daemon/proxy_lifecycle_test.go +2 -2
@@ -581,7 +581,7 @@ func TestReplacedProxyCannotBeSelectedOrReattachedAfterSwitchAway(t *testing.T)
581581 require.True(t, ok)
582582 require.Equal(t, proxyWarmDuration, warmTimer.delay)
583583
584- views, _ := d.pickerViews(local)
584+ views, _ := d.pickerViews(local, nil)
585585 var expiredView picker.SessionView
586586 for _, view := range views {
587587 if view.RemoteKey != nil && *view.RemoteKey == key {
@@ -597,7 +597,7 @@ func TestReplacedProxyCannotBeSelectedOrReattachedAfterSwitchAway(t *testing.T)
597597 require.Equal(t, key.Display()+" [expired]", proxy.lifecycleDisplayName())
598598
599599 d.enterPicker(local, ac)
600- forced := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{
600+ forced := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{
601601 Session: key.ID(), RemoteKey: &key,
602602 })
603603 ac.overlays.pickerMu.Lock()
@@ -581,7 +581,7 @@ func TestReplacedProxyCannotBeSelectedOrReattachedAfterSwitchAway(t *testing.T)
581 require.True(t, ok)581 require.True(t, ok)
582 require.Equal(t, proxyWarmDuration, warmTimer.delay)582 require.Equal(t, proxyWarmDuration, warmTimer.delay)
583 583
584- views, _ := d.pickerViews(local)584+ views, _ := d.pickerViews(local, nil)
585 var expiredView picker.SessionView585 var expiredView picker.SessionView
586 for _, view := range views {586 for _, view := range views {
587 if view.RemoteKey != nil && *view.RemoteKey == key {587 if view.RemoteKey != nil && *view.RemoteKey == key {
@@ -597,7 +597,7 @@ func TestReplacedProxyCannotBeSelectedOrReattachedAfterSwitchAway(t *testing.T)
597 require.Equal(t, key.Display()+" [expired]", proxy.lifecycleDisplayName())597 require.Equal(t, key.Display()+" [expired]", proxy.lifecycleDisplayName())
598 598
599 d.enterPicker(local, ac)599 d.enterPicker(local, ac)
600- forced := d.newPickerModel(local, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{600+ forced := d.newPickerModel(local, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{
601 Session: key.ID(), RemoteKey: &key,601 Session: key.ID(), RemoteKey: &key,
602 })602 })
603 ac.overlays.pickerMu.Lock()603 ac.overlays.pickerMu.Lock()
M internal/usecase/daemon/proxy_session.go +2 -2
@@ -148,7 +148,7 @@ func (p *proxySession) snapshotView(opts viewOptions) sessionView {
148148 expired: expired,
149149 createdAt: p.createdAt,
150150 mruAt: p.mruAt.Load(),
151- active: int(meta.Active),
151+ defaultTab: 0,
152152 tabCount: len(meta.Tabs),
153153 cannotAcceptMoves: true,
154154 }
@@ -191,7 +191,7 @@ func (p *proxySession) statusSegments(_ bool) statusSnapshot {
191191 return snapshot
192192 }
193193
194-func (p *proxySession) activateTargetLocked(tabIndex int) bool {
194+func (p *proxySession) validTargetTabLocked(tabIndex int) bool {
195195 if tabIndex < 0 {
196196 return true
197197 }
@@ -148,7 +148,7 @@ func (p *proxySession) snapshotView(opts viewOptions) sessionView {
148 expired: expired,148 expired: expired,
149 createdAt: p.createdAt,149 createdAt: p.createdAt,
150 mruAt: p.mruAt.Load(),150 mruAt: p.mruAt.Load(),
151- active: int(meta.Active),151+ defaultTab: 0,
152 tabCount: len(meta.Tabs),152 tabCount: len(meta.Tabs),
153 cannotAcceptMoves: true,153 cannotAcceptMoves: true,
154 }154 }
@@ -191,7 +191,7 @@ func (p *proxySession) statusSegments(_ bool) statusSnapshot {
191 return snapshot191 return snapshot
192 }192 }
193 193
194-func (p *proxySession) activateTargetLocked(tabIndex int) bool {194+func (p *proxySession) validTargetTabLocked(tabIndex int) bool {
195 if tabIndex < 0 {195 if tabIndex < 0 {
196 return true196 return true
197 }197 }
M internal/usecase/daemon/remote_picker_test.go +22 -22
@@ -83,7 +83,7 @@ func TestRemotePickerMerge(t *testing.T) {
8383 d.remoteCatalog.status["learned"] = remoteHostFresh
8484 d.remoteCatalog.mu.Unlock()
8585
86- views, _ := d.pickerViews(local)
86+ views, _ := d.pickerViews(local, nil)
8787
8888 require.Equal(t, []string{"local", "work@pinned", "alpha@pinned", "alpha@learned", "stopped"}, pickerViewNames(views))
8989 require.False(t, views[0].Stopped)
@@ -113,7 +113,7 @@ func TestRemotePickerDedupeUsesExactKeyAndKeepsLiveID(t *testing.T) {
113113 },
114114 }})
115115
116- views, _ := d.pickerViews(nil)
116+ views, _ := d.pickerViews(nil, nil)
117117
118118 require.Len(t, views, 1)
119119 require.Equal(t, key.ID(), views[0].ID)
@@ -132,7 +132,7 @@ func TestRemotePickerSnapshotsDoNotRetainProxyLock(t *testing.T) {
132132 }
133133 d.sessions[key.ID()] = proxy
134134
135- views, _ := d.pickerViews(nil)
135+ views, _ := d.pickerViews(nil, nil)
136136
137137 require.Len(t, views, 1)
138138 require.True(t, proxy.mu.TryLock(), "picker construction must release the proxy leaf lock")
@@ -162,7 +162,7 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
162162 d.remoteCatalog.status["arch"] = test.status
163163 d.remoteCatalog.mu.Unlock()
164164
165- views, _ := d.pickerViews(nil)
165+ views, _ := d.pickerViews(nil, nil)
166166
167167 require.Len(t, views, 1)
168168 require.Equal(t, test.want, views[0].RemoteAvailability)
@@ -182,7 +182,7 @@ func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) {
182182 d.remoteCatalog.failure["arch"] = failedAt
183183 d.remoteCatalog.mu.Unlock()
184184
185- views, _ := d.pickerViews(nil)
185+ views, _ := d.pickerViews(nil, nil)
186186
187187 require.Len(t, views, 1)
188188 require.Equal(t, "stale since "+fetchedAt.Format(time.RFC3339), views[0].RemoteDetail)
@@ -206,7 +206,7 @@ func TestRemotePickerNoCacheHostFailuresRemainVisible(t *testing.T) {
206206 d.remoteCatalog.failure["arch"] = time.Unix(50, 0)
207207 d.remoteCatalog.mu.Unlock()
208208
209- views, _ := d.pickerViews(nil)
209+ views, _ := d.pickerViews(nil, nil)
210210
211211 require.Len(t, views, 1)
212212 require.Equal(t, "arch", views[0].Name)
@@ -233,7 +233,7 @@ func TestRemotePickerGroupedSortTreatsProxiesAsLiveSessions(t *testing.T) {
233233 d.sessions[proxy.id] = proxy
234234 d.sessions[named.id] = named
235235
236- views, _ := d.pickerViews(nil)
236+ views, _ := d.pickerViews(nil, nil)
237237
238238 require.Equal(t, []string{"named", "work@arch", "1"}, pickerViewNames(views))
239239 }
@@ -539,7 +539,7 @@ func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID) (
539539 ac.initOverlays()
540540 tb := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
541541 tb.stableID = string(id) + "-tab"
542- sess := &session{sessionCore: sessionCore{id: id, name: string(id), attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, active: 0}
542+ sess := &session{sessionCore: sessionCore{id: id, name: string(id), attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}}
543543 publishTiledPaneOwners(sess, tb)
544544 ac.setSession(sess)
545545 d.sessions[id] = sess
@@ -555,7 +555,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
555555
556556 for _, owner := range []*attachedClient{first, second} {
557557 owner.overlays.pickerMu.Lock()
558- owner.overlays.picker = d.newPickerModel(firstSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: "second", TabID: "second-tab"})
558+ owner.overlays.picker = d.newPickerModel(firstSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: "second", TabID: "second-tab"})
559559 owner.overlays.pickerIntent = pickerNavigate
560560 owner.overlays.pickerGeneration++
561561 instance := remotePickerInstance{ac: owner, generation: owner.overlays.pickerGeneration, model: owner.overlays.picker}
@@ -600,7 +600,7 @@ func TestRemotePickerTeardownLifecycle(t *testing.T) {
600600 d := newRemotePickerDaemon(nil)
601601 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
602602 ac.resumeCapable = test.resumeCapable
603- model := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
603+ model := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
604604 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})
605605 ac.overlays.pickerMu.Lock()
606606 generation := ac.overlays.pickerGeneration
@@ -639,7 +639,7 @@ func TestParkedPickerExpiryClosesCapturedGenerationAndCancelsRefresh(t *testing.
639639 d.clock = clock
640640 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
641641 ac.resumeCapable = true
642- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
642+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
643643 request := receiveRemotePicker(t, catalog.requests, "catalog request")
644644
645645 require.True(t, d.parkAttachment(sess, ac))
@@ -659,7 +659,7 @@ func TestParkedPickerRetirementClosesCapturedGenerationOnly(t *testing.T) {
659659 d := newRemotePickerDaemon(nil)
660660 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
661661 ac.resumeCapable = true
662- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
662+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
663663 d.refreshPickerOpts(ac, pickerRefreshOptions{preserveSelection: true, nearestRow: -1})
664664 ac.overlays.pickerMu.Lock()
665665 refreshed := ac.overlays.picker
@@ -686,10 +686,10 @@ func TestParkedPickerTokenReplacementRetiresPreviousGeneration(t *testing.T) {
686686 firstSession, first, _ := addRemoteRefreshPickerOwner(t, d, "first")
687687 secondSession, second, _ := addRemoteRefreshPickerOwner(t, d, "second")
688688 first.resumeCapable, second.resumeCapable = true, true
689- d.publishPicker(firstSession, first, d.newPickerModel(firstSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
689+ d.publishPicker(firstSession, first, d.newPickerModel(firstSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
690690 require.True(t, d.parkAttachment(firstSession, first))
691691 token := first.resumeToken
692- d.publishPicker(secondSession, second, d.newPickerModel(secondSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
692+ d.publishPicker(secondSession, second, d.newPickerModel(secondSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
693693 second.resumeToken = token
694694 require.True(t, d.parkAttachment(secondSession, second))
695695
@@ -706,7 +706,7 @@ func TestParkedPickerResumePreservesGeneration(t *testing.T) {
706706 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
707707 ac.resumeCapable = true
708708 ac.clientID = [16]byte{1, 2, 3, 4}
709- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
709+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
710710 ac.overlays.pickerMu.Lock()
711711 model, generation := ac.overlays.picker, ac.overlays.pickerGeneration
712712 ac.overlays.pickerMu.Unlock()
@@ -728,7 +728,7 @@ func TestStaleParkedPickerExpiryPreservesNewGeneration(t *testing.T) {
728728 d := newRemotePickerDaemon(nil)
729729 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
730730 ac.resumeCapable = true
731- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
731+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
732732 require.True(t, d.parkAttachment(sess, ac))
733733 token := ac.resumeToken
734734 d.mu.Lock()
@@ -736,7 +736,7 @@ func TestStaleParkedPickerExpiryPreservesNewGeneration(t *testing.T) {
736736 d.mu.Unlock()
737737 require.NotNil(t, parked)
738738
739- newer := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
739+ newer := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
740740 d.publishPicker(sess, ac, newer, pickerNavigate, moveSourceLocator{})
741741 d.expireParked(token, parked)
742742 ac.overlays.pickerMu.Lock()
@@ -752,7 +752,7 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing.
752752 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},
753753 }})
754754 owner.overlays.pickerMu.Lock()
755- owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
755+ owner.overlays.picker = d.newPickerModel(ownerSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
756756 owner.overlays.pickerIntent = pickerNavigate
757757 before, ok := owner.overlays.picker.Cursor()
758758 _, beforeSelectable := owner.overlays.picker.Selected()
@@ -864,7 +864,7 @@ func TestRemoteRefreshCannotStartAfterLastPickerCloseWins(t *testing.T) {
864864 func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
865865 d := newRemotePickerDaemon(nil)
866866 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
867- first := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
867+ first := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
868868 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})
869869
870870 staleClose := d.pickerListInputState(ac)
@@ -872,7 +872,7 @@ func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
872872 staleClose.closeLocked()
873873 ac.overlays.pickerMu.Unlock()
874874
875- reopened := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
875+ reopened := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
876876 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})
877877 staleClose.afterClose()
878878
@@ -888,7 +888,7 @@ func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
888888 func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
889889 d := newRemotePickerDaemon(nil)
890890 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
891- first := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
891+ first := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
892892 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})
893893
894894 rebuildReached := make(chan struct{})
@@ -906,7 +906,7 @@ func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
906906 receiveRemotePickerClose(t, rebuildReached, "picker rebuild")
907907
908908 d.closePicker(ac)
909- reopened := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
909+ reopened := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
910910 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})
911911 close(allowPublication)
912912 require.NoError(t, receiveRemotePicker(t, publicationWait, "picker publication release"))
@@ -83,7 +83,7 @@ func TestRemotePickerMerge(t *testing.T) {
83 d.remoteCatalog.status["learned"] = remoteHostFresh83 d.remoteCatalog.status["learned"] = remoteHostFresh
84 d.remoteCatalog.mu.Unlock()84 d.remoteCatalog.mu.Unlock()
85 85
86- views, _ := d.pickerViews(local)86+ views, _ := d.pickerViews(local, nil)
87 87
88 require.Equal(t, []string{"local", "work@pinned", "alpha@pinned", "alpha@learned", "stopped"}, pickerViewNames(views))88 require.Equal(t, []string{"local", "work@pinned", "alpha@pinned", "alpha@learned", "stopped"}, pickerViewNames(views))
89 require.False(t, views[0].Stopped)89 require.False(t, views[0].Stopped)
@@ -113,7 +113,7 @@ func TestRemotePickerDedupeUsesExactKeyAndKeepsLiveID(t *testing.T) {
113 },113 },
114 }})114 }})
115 115
116- views, _ := d.pickerViews(nil)116+ views, _ := d.pickerViews(nil, nil)
117 117
118 require.Len(t, views, 1)118 require.Len(t, views, 1)
119 require.Equal(t, key.ID(), views[0].ID)119 require.Equal(t, key.ID(), views[0].ID)
@@ -132,7 +132,7 @@ func TestRemotePickerSnapshotsDoNotRetainProxyLock(t *testing.T) {
132 }132 }
133 d.sessions[key.ID()] = proxy133 d.sessions[key.ID()] = proxy
134 134
135- views, _ := d.pickerViews(nil)135+ views, _ := d.pickerViews(nil, nil)
136 136
137 require.Len(t, views, 1)137 require.Len(t, views, 1)
138 require.True(t, proxy.mu.TryLock(), "picker construction must release the proxy leaf lock")138 require.True(t, proxy.mu.TryLock(), "picker construction must release the proxy leaf lock")
@@ -162,7 +162,7 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) {
162 d.remoteCatalog.status["arch"] = test.status162 d.remoteCatalog.status["arch"] = test.status
163 d.remoteCatalog.mu.Unlock()163 d.remoteCatalog.mu.Unlock()
164 164
165- views, _ := d.pickerViews(nil)165+ views, _ := d.pickerViews(nil, nil)
166 166
167 require.Len(t, views, 1)167 require.Len(t, views, 1)
168 require.Equal(t, test.want, views[0].RemoteAvailability)168 require.Equal(t, test.want, views[0].RemoteAvailability)
@@ -182,7 +182,7 @@ func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) {
182 d.remoteCatalog.failure["arch"] = failedAt182 d.remoteCatalog.failure["arch"] = failedAt
183 d.remoteCatalog.mu.Unlock()183 d.remoteCatalog.mu.Unlock()
184 184
185- views, _ := d.pickerViews(nil)185+ views, _ := d.pickerViews(nil, nil)
186 186
187 require.Len(t, views, 1)187 require.Len(t, views, 1)
188 require.Equal(t, "stale since "+fetchedAt.Format(time.RFC3339), views[0].RemoteDetail)188 require.Equal(t, "stale since "+fetchedAt.Format(time.RFC3339), views[0].RemoteDetail)
@@ -206,7 +206,7 @@ func TestRemotePickerNoCacheHostFailuresRemainVisible(t *testing.T) {
206 d.remoteCatalog.failure["arch"] = time.Unix(50, 0)206 d.remoteCatalog.failure["arch"] = time.Unix(50, 0)
207 d.remoteCatalog.mu.Unlock()207 d.remoteCatalog.mu.Unlock()
208 208
209- views, _ := d.pickerViews(nil)209+ views, _ := d.pickerViews(nil, nil)
210 210
211 require.Len(t, views, 1)211 require.Len(t, views, 1)
212 require.Equal(t, "arch", views[0].Name)212 require.Equal(t, "arch", views[0].Name)
@@ -233,7 +233,7 @@ func TestRemotePickerGroupedSortTreatsProxiesAsLiveSessions(t *testing.T) {
233 d.sessions[proxy.id] = proxy233 d.sessions[proxy.id] = proxy
234 d.sessions[named.id] = named234 d.sessions[named.id] = named
235 235
236- views, _ := d.pickerViews(nil)236+ views, _ := d.pickerViews(nil, nil)
237 237
238 require.Equal(t, []string{"named", "work@arch", "1"}, pickerViewNames(views))238 require.Equal(t, []string{"named", "work@arch", "1"}, pickerViewNames(views))
239 }239 }
@@ -539,7 +539,7 @@ func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID) (
539 ac.initOverlays()539 ac.initOverlays()
540 tb := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 22})540 tb := newTab(newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
541 tb.stableID = string(id) + "-tab"541 tb.stableID = string(id) + "-tab"
542- sess := &session{sessionCore: sessionCore{id: id, name: string(id), attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, active: 0}542+ sess := &session{sessionCore: sessionCore{id: id, name: string(id), attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}}
543 publishTiledPaneOwners(sess, tb)543 publishTiledPaneOwners(sess, tb)
544 ac.setSession(sess)544 ac.setSession(sess)
545 d.sessions[id] = sess545 d.sessions[id] = sess
@@ -555,7 +555,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
555 555
556 for _, owner := range []*attachedClient{first, second} {556 for _, owner := range []*attachedClient{first, second} {
557 owner.overlays.pickerMu.Lock()557 owner.overlays.pickerMu.Lock()
558- owner.overlays.picker = d.newPickerModel(firstSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: "second", TabID: "second-tab"})558+ owner.overlays.picker = d.newPickerModel(firstSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: "second", TabID: "second-tab"})
559 owner.overlays.pickerIntent = pickerNavigate559 owner.overlays.pickerIntent = pickerNavigate
560 owner.overlays.pickerGeneration++560 owner.overlays.pickerGeneration++
561 instance := remotePickerInstance{ac: owner, generation: owner.overlays.pickerGeneration, model: owner.overlays.picker}561 instance := remotePickerInstance{ac: owner, generation: owner.overlays.pickerGeneration, model: owner.overlays.picker}
@@ -600,7 +600,7 @@ func TestRemotePickerTeardownLifecycle(t *testing.T) {
600 d := newRemotePickerDaemon(nil)600 d := newRemotePickerDaemon(nil)
601 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")601 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
602 ac.resumeCapable = test.resumeCapable602 ac.resumeCapable = test.resumeCapable
603- model := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})603+ model := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
604 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})604 d.publishPicker(sess, ac, model, pickerNavigate, moveSourceLocator{})
605 ac.overlays.pickerMu.Lock()605 ac.overlays.pickerMu.Lock()
606 generation := ac.overlays.pickerGeneration606 generation := ac.overlays.pickerGeneration
@@ -639,7 +639,7 @@ func TestParkedPickerExpiryClosesCapturedGenerationAndCancelsRefresh(t *testing.
639 d.clock = clock639 d.clock = clock
640 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")640 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
641 ac.resumeCapable = true641 ac.resumeCapable = true
642- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})642+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
643 request := receiveRemotePicker(t, catalog.requests, "catalog request")643 request := receiveRemotePicker(t, catalog.requests, "catalog request")
644 644
645 require.True(t, d.parkAttachment(sess, ac))645 require.True(t, d.parkAttachment(sess, ac))
@@ -659,7 +659,7 @@ func TestParkedPickerRetirementClosesCapturedGenerationOnly(t *testing.T) {
659 d := newRemotePickerDaemon(nil)659 d := newRemotePickerDaemon(nil)
660 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")660 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
661 ac.resumeCapable = true661 ac.resumeCapable = true
662- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})662+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
663 d.refreshPickerOpts(ac, pickerRefreshOptions{preserveSelection: true, nearestRow: -1})663 d.refreshPickerOpts(ac, pickerRefreshOptions{preserveSelection: true, nearestRow: -1})
664 ac.overlays.pickerMu.Lock()664 ac.overlays.pickerMu.Lock()
665 refreshed := ac.overlays.picker665 refreshed := ac.overlays.picker
@@ -686,10 +686,10 @@ func TestParkedPickerTokenReplacementRetiresPreviousGeneration(t *testing.T) {
686 firstSession, first, _ := addRemoteRefreshPickerOwner(t, d, "first")686 firstSession, first, _ := addRemoteRefreshPickerOwner(t, d, "first")
687 secondSession, second, _ := addRemoteRefreshPickerOwner(t, d, "second")687 secondSession, second, _ := addRemoteRefreshPickerOwner(t, d, "second")
688 first.resumeCapable, second.resumeCapable = true, true688 first.resumeCapable, second.resumeCapable = true, true
689- d.publishPicker(firstSession, first, d.newPickerModel(firstSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})689+ d.publishPicker(firstSession, first, d.newPickerModel(firstSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
690 require.True(t, d.parkAttachment(firstSession, first))690 require.True(t, d.parkAttachment(firstSession, first))
691 token := first.resumeToken691 token := first.resumeToken
692- d.publishPicker(secondSession, second, d.newPickerModel(secondSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})692+ d.publishPicker(secondSession, second, d.newPickerModel(secondSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
693 second.resumeToken = token693 second.resumeToken = token
694 require.True(t, d.parkAttachment(secondSession, second))694 require.True(t, d.parkAttachment(secondSession, second))
695 695
@@ -706,7 +706,7 @@ func TestParkedPickerResumePreservesGeneration(t *testing.T) {
706 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")706 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
707 ac.resumeCapable = true707 ac.resumeCapable = true
708 ac.clientID = [16]byte{1, 2, 3, 4}708 ac.clientID = [16]byte{1, 2, 3, 4}
709- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})709+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
710 ac.overlays.pickerMu.Lock()710 ac.overlays.pickerMu.Lock()
711 model, generation := ac.overlays.picker, ac.overlays.pickerGeneration711 model, generation := ac.overlays.picker, ac.overlays.pickerGeneration
712 ac.overlays.pickerMu.Unlock()712 ac.overlays.pickerMu.Unlock()
@@ -728,7 +728,7 @@ func TestStaleParkedPickerExpiryPreservesNewGeneration(t *testing.T) {
728 d := newRemotePickerDaemon(nil)728 d := newRemotePickerDaemon(nil)
729 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")729 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
730 ac.resumeCapable = true730 ac.resumeCapable = true
731- d.publishPicker(sess, ac, d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})731+ d.publishPicker(sess, ac, d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{}), pickerNavigate, moveSourceLocator{})
732 require.True(t, d.parkAttachment(sess, ac))732 require.True(t, d.parkAttachment(sess, ac))
733 token := ac.resumeToken733 token := ac.resumeToken
734 d.mu.Lock()734 d.mu.Lock()
@@ -736,7 +736,7 @@ func TestStaleParkedPickerExpiryPreservesNewGeneration(t *testing.T) {
736 d.mu.Unlock()736 d.mu.Unlock()
737 require.NotNil(t, parked)737 require.NotNil(t, parked)
738 738
739- newer := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})739+ newer := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
740 d.publishPicker(sess, ac, newer, pickerNavigate, moveSourceLocator{})740 d.publishPicker(sess, ac, newer, pickerNavigate, moveSourceLocator{})
741 d.expireParked(token, parked)741 d.expireParked(token, parked)
742 ac.overlays.pickerMu.Lock()742 ac.overlays.pickerMu.Lock()
@@ -752,7 +752,7 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing.
752 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},752 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: key.Name, State: "running"}},
753 }})753 }})
754 owner.overlays.pickerMu.Lock()754 owner.overlays.pickerMu.Lock()
755- owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})755+ owner.overlays.picker = d.newPickerModel(ownerSession, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
756 owner.overlays.pickerIntent = pickerNavigate756 owner.overlays.pickerIntent = pickerNavigate
757 before, ok := owner.overlays.picker.Cursor()757 before, ok := owner.overlays.picker.Cursor()
758 _, beforeSelectable := owner.overlays.picker.Selected()758 _, beforeSelectable := owner.overlays.picker.Selected()
@@ -864,7 +864,7 @@ func TestRemoteRefreshCannotStartAfterLastPickerCloseWins(t *testing.T) {
864 func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {864 func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
865 d := newRemotePickerDaemon(nil)865 d := newRemotePickerDaemon(nil)
866 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")866 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
867- first := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})867+ first := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
868 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})868 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})
869 869
870 staleClose := d.pickerListInputState(ac)870 staleClose := d.pickerListInputState(ac)
@@ -872,7 +872,7 @@ func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
872 staleClose.closeLocked()872 staleClose.closeLocked()
873 ac.overlays.pickerMu.Unlock()873 ac.overlays.pickerMu.Unlock()
874 874
875- reopened := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})875+ reopened := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
876 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})876 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})
877 staleClose.afterClose()877 staleClose.afterClose()
878 878
@@ -888,7 +888,7 @@ func TestRemotePickerStaleAfterCloseCannotRemoveReopenedPicker(t *testing.T) {
888 func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {888 func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
889 d := newRemotePickerDaemon(nil)889 d := newRemotePickerDaemon(nil)
890 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")890 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "owner")
891- first := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})891+ first := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
892 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})892 d.publishPicker(sess, ac, first, pickerNavigate, moveSourceLocator{})
893 893
894 rebuildReached := make(chan struct{})894 rebuildReached := make(chan struct{})
@@ -906,7 +906,7 @@ func TestRemotePickerStaleRefreshCannotOverwriteReopenedModel(t *testing.T) {
906 receiveRemotePickerClose(t, rebuildReached, "picker rebuild")906 receiveRemotePickerClose(t, rebuildReached, "picker rebuild")
907 907
908 d.closePicker(ac)908 d.closePicker(ac)
909- reopened := d.newPickerModel(sess, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})909+ reopened := d.newPickerModel(sess, nil, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{})
910 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})910 d.publishPicker(sess, ac, reopened, pickerNavigate, moveSourceLocator{})
911 close(allowPublication)911 close(allowPublication)
912 require.NoError(t, receiveRemotePicker(t, publicationWait, "picker publication release"))912 require.NoError(t, receiveRemotePicker(t, publicationWait, "picker publication release"))
M internal/usecase/daemon/render.go +1 -1
@@ -319,7 +319,7 @@ func (d *Daemon) paint(entry attachmentSession, ac *attachedClient, reset bool,
319319 }
320320 var tb *tab
321321 if local {
322- tb = sess.tabForAttachmentOrActive(ac)
322+ tb = sess.tabForAttachment(ac)
323323 if tb == nil {
324324 return
325325 }
@@ -319,7 +319,7 @@ func (d *Daemon) paint(entry attachmentSession, ac *attachedClient, reset bool,
319 }319 }
320 var tb *tab320 var tb *tab
321 if local {321 if local {
322- tb = sess.tabForAttachmentOrActive(ac)322+ tb = sess.tabForAttachment(ac)
323 if tb == nil {323 if tb == nil {
324 return324 return
325 }325 }
M internal/usecase/daemon/render_capture.go +1 -1
@@ -219,7 +219,7 @@ func captureLocalPrimaryRenderState(
219219 }
220220 tb, _ := sess.paneForAttachment(ac)
221221 if tb == nil {
222- tb = sess.tabForAttachmentOrActive(ac)
222+ tb = sess.tabForAttachment(ac)
223223 }
224224 if tb == nil {
225225 return nil, false
@@ -219,7 +219,7 @@ func captureLocalPrimaryRenderState(
219 }219 }
220 tb, _ := sess.paneForAttachment(ac)220 tb, _ := sess.paneForAttachment(ac)
221 if tb == nil {221 if tb == nil {
222- tb = sess.tabForAttachmentOrActive(ac)222+ tb = sess.tabForAttachment(ac)
223 }223 }
224 if tb == nil {224 if tb == nil {
225 return nil, false225 return nil, false
M internal/usecase/daemon/render_coordinator_test.go +3 -3
@@ -1116,7 +1116,7 @@ func TestProducerInvalidations(t *testing.T) {
11161116 name: "floating toggle to visible",
11171117 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
11181118 fp := newPaneWithStableID(layout.PaneID("floating"), "float-producer", newQuietPTY(), domain.Size{Cols: 40, Rows: 10})
1119- tb := sess.activeTab()
1119+ tb := testAttachmentTab(sess)
11201120 tb.mu.Lock()
11211121 tb.floating.pane = fp
11221122 tb.floating.state = floatingHidden
@@ -1134,7 +1134,7 @@ func TestProducerInvalidations(t *testing.T) {
11341134 ac.proxied = true
11351135 sess.tabs[1].attention = true
11361136 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionJumpAttention, nil)
1137- require.Equal(t, 1, activeTabIndex(sess))
1137+ require.Equal(t, 1, testAttachmentTabIndex(sess))
11381138 },
11391139 },
11401140 {
@@ -1275,7 +1275,7 @@ func TestConcurrentPaintInitializesOverlayUnderSendOwnership(t *testing.T) {
12751275 func TestStartPaneGoroutinesAccountsForOneReader(t *testing.T) {
12761276 p, release := newBlockingPTY(t)
12771277 d, sess, _, _ := newManualSessionWithPTYs(t, p)
1278- tb := sess.activeTab()
1278+ tb := testAttachmentTab(sess)
12791279 require.NotNil(t, tb)
12801280 d.startPaneGoroutines(sess, tb, tb.focusedPane())
12811281 release()
@@ -1116,7 +1116,7 @@ func TestProducerInvalidations(t *testing.T) {
1116 name: "floating toggle to visible",1116 name: "floating toggle to visible",
1117 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {1117 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
1118 fp := newPaneWithStableID(layout.PaneID("floating"), "float-producer", newQuietPTY(), domain.Size{Cols: 40, Rows: 10})1118 fp := newPaneWithStableID(layout.PaneID("floating"), "float-producer", newQuietPTY(), domain.Size{Cols: 40, Rows: 10})
1119- tb := sess.activeTab()1119+ tb := testAttachmentTab(sess)
1120 tb.mu.Lock()1120 tb.mu.Lock()
1121 tb.floating.pane = fp1121 tb.floating.pane = fp
1122 tb.floating.state = floatingHidden1122 tb.floating.state = floatingHidden
@@ -1134,7 +1134,7 @@ func TestProducerInvalidations(t *testing.T) {
1134 ac.proxied = true1134 ac.proxied = true
1135 sess.tabs[1].attention = true1135 sess.tabs[1].attention = true
1136 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionJumpAttention, nil)1136 daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionJumpAttention, nil)
1137- require.Equal(t, 1, activeTabIndex(sess))1137+ require.Equal(t, 1, testAttachmentTabIndex(sess))
1138 },1138 },
1139 },1139 },
1140 {1140 {
@@ -1275,7 +1275,7 @@ func TestConcurrentPaintInitializesOverlayUnderSendOwnership(t *testing.T) {
1275 func TestStartPaneGoroutinesAccountsForOneReader(t *testing.T) {1275 func TestStartPaneGoroutinesAccountsForOneReader(t *testing.T) {
1276 p, release := newBlockingPTY(t)1276 p, release := newBlockingPTY(t)
1277 d, sess, _, _ := newManualSessionWithPTYs(t, p)1277 d, sess, _, _ := newManualSessionWithPTYs(t, p)
1278- tb := sess.activeTab()1278+ tb := testAttachmentTab(sess)
1279 require.NotNil(t, tb)1279 require.NotNil(t, tb)
1280 d.startPaneGoroutines(sess, tb, tb.focusedPane())1280 d.startPaneGoroutines(sess, tb, tb.focusedPane())
1281 release()1281 release()
M internal/usecase/daemon/render_test.go +11 -11
@@ -79,7 +79,7 @@ func TestFirstPaintRetainedFloatingPaneEmitsOneReset(t *testing.T) {
7979 // outer terminal also changes, its completed transaction must cover
8080 // that popup rather than emitting a second reset-producing frame.
8181 floating := newPane(layout.PaneID("floating"), nil, domain.Size{Cols: 20, Rows: 8})
82- installTestFloating(sess.activeTab(), floating, true)
82+ installTestFloating(testAttachmentTab(sess), floating, true)
8383
8484 d.firstPaint(sess, ac, tc.clientSize)
8585
@@ -321,7 +321,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) {
321321 awaitPTYReadProcessed(t, inactiveProcessed)
322322
323323 sess.mu.Lock()
324- sess.active = 1
324+ selectTestAttachmentTabLocked(sess, 1)
325325 sess.mu.Unlock()
326326 d.invalidateRender(sess, ac, true, "sync activation")
327327 fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), urgentRenderDeadline)
@@ -358,7 +358,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) {
358358 awaitPTYReadProcessed(t, oldProcessed)
359359
360360 sess.mu.Lock()
361- sess.active = 1
361+ selectTestAttachmentTabLocked(sess, 1)
362362 sess.mu.Unlock()
363363 newSteps <- channelPTYStep{data: []byte("newly active")}
364364 awaitPTYReadProcessed(t, newProcessed)
@@ -601,7 +601,7 @@ func TestPaneRenderableActiveAttachmentDoesNotScanPickerPreviews(t *testing.T) {
601601 p, release := newBlockingPTY(t)
602602 defer release()
603603 d, sess, _, _ := newManualSessionWithPTYs(t, p)
604- tb := sess.activeTab()
604+ tb := testAttachmentTab(sess)
605605 pane := tb.focusedPane()
606606
607607 // Holding daemon ownership makes a picker scan block. An active attached
@@ -632,7 +632,7 @@ func TestNonRenderablePaneDamageRemainsPendingForCapture(t *testing.T) {
632632 {name: "inactive tab", setup: func(_ *Daemon, sess *session, tb *tab, _ *pane) {
633633 other := newTab(nil, domain.Size{Cols: 80, Rows: 23})
634634 sess.tabs = append([]*tab{other}, sess.tabs...)
635- sess.active = 0
635+ selectTestAttachmentTab(sess, 0)
636636 require.NotSame(t, other, tb)
637637 }},
638638 {name: "collapsed", setup: func(_ *Daemon, _ *session, tb *tab, p *pane) {
@@ -719,7 +719,7 @@ func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
719719 p2, releasePTY2 := newBlockingPTY(t)
720720 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
721721 defer releasePTY2()
722- sess.active = 0
722+ selectTestAttachmentTab(sess, 0)
723723 sess.tabs[1].focusedPane().screen.Write([]byte("remaining"))
724724
725725 d.sessWg.Add(1)
@@ -728,7 +728,7 @@ func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
728728
729729 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)
730730 require.Equal(t, 1, sessionCount(d))
731- require.Equal(t, 0, activeTabIndex(sess))
731+ require.Equal(t, 0, testAttachmentTabIndex(sess))
732732 f := awaitFrame(t, sends, ports.MsgOutput)
733733 out, err := ports.UnmarshalOutput(f.Payload)
734734 require.NoError(t, err)
@@ -745,7 +745,7 @@ func TestPTYEOFClosesInactiveNonFinalTabAndRepaintsStatus(t *testing.T) {
745745 p2, releasePTY2 := newBlockingPTY(t)
746746 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
747747 defer releasePTY1()
748- sess.active = 0
748+ selectTestAttachmentTab(sess, 0)
749749 sess.tabs[0].focusedPane().screen.Write([]byte("active"))
750750
751751 d.sessWg.Add(1)
@@ -754,7 +754,7 @@ func TestPTYEOFClosesInactiveNonFinalTabAndRepaintsStatus(t *testing.T) {
754754
755755 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)
756756 require.Equal(t, 1, sessionCount(d))
757- require.Equal(t, 0, activeTabIndex(sess))
757+ require.Equal(t, 0, testAttachmentTabIndex(sess))
758758 f := awaitFrame(t, sends, ports.MsgOutput)
759759 out, err := ports.UnmarshalOutput(f.Payload)
760760 require.NoError(t, err)
@@ -1198,7 +1198,7 @@ func TestPaintAlignsFloatingCursorWithCommittedGeometry(t *testing.T) {
11981198 floating := newPane("floating", nil, rectSize(committed.Inner))
11991199 floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'})
12001200 floating.popupGeometry = committed
1201- installTestFloating(sess.activeTab(), floating, true)
1201+ installTestFloating(testAttachmentTab(sess), floating, true)
12021202
12031203 d.paint(sess, ac, true, nil)
12041204 data := mustOutputData(t, sends)
@@ -1300,7 +1300,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) {
13001300 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})
13011301 appendHistoryRow(t, fp.history, testRow("flt-old"))
13021302 fp.screen.Write([]byte("flt-live"))
1303- installTestFloating(sess.activeTab(), fp, true)
1303+ installTestFloating(testAttachmentTab(sess), fp, true)
13041304
13051305 d.enterCopyMode(sess, ac)
13061306 data := mustOutputData(t, sends)
@@ -79,7 +79,7 @@ func TestFirstPaintRetainedFloatingPaneEmitsOneReset(t *testing.T) {
79 // outer terminal also changes, its completed transaction must cover79 // outer terminal also changes, its completed transaction must cover
80 // that popup rather than emitting a second reset-producing frame.80 // that popup rather than emitting a second reset-producing frame.
81 floating := newPane(layout.PaneID("floating"), nil, domain.Size{Cols: 20, Rows: 8})81 floating := newPane(layout.PaneID("floating"), nil, domain.Size{Cols: 20, Rows: 8})
82- installTestFloating(sess.activeTab(), floating, true)82+ installTestFloating(testAttachmentTab(sess), floating, true)
83 83
84 d.firstPaint(sess, ac, tc.clientSize)84 d.firstPaint(sess, ac, tc.clientSize)
85 85
@@ -321,7 +321,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) {
321 awaitPTYReadProcessed(t, inactiveProcessed)321 awaitPTYReadProcessed(t, inactiveProcessed)
322 322
323 sess.mu.Lock()323 sess.mu.Lock()
324- sess.active = 1324+ selectTestAttachmentTabLocked(sess, 1)
325 sess.mu.Unlock()325 sess.mu.Unlock()
326 d.invalidateRender(sess, ac, true, "sync activation")326 d.invalidateRender(sess, ac, true, "sync activation")
327 fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), urgentRenderDeadline)327 fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), urgentRenderDeadline)
@@ -358,7 +358,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) {
358 awaitPTYReadProcessed(t, oldProcessed)358 awaitPTYReadProcessed(t, oldProcessed)
359 359
360 sess.mu.Lock()360 sess.mu.Lock()
361- sess.active = 1361+ selectTestAttachmentTabLocked(sess, 1)
362 sess.mu.Unlock()362 sess.mu.Unlock()
363 newSteps <- channelPTYStep{data: []byte("newly active")}363 newSteps <- channelPTYStep{data: []byte("newly active")}
364 awaitPTYReadProcessed(t, newProcessed)364 awaitPTYReadProcessed(t, newProcessed)
@@ -601,7 +601,7 @@ func TestPaneRenderableActiveAttachmentDoesNotScanPickerPreviews(t *testing.T) {
601 p, release := newBlockingPTY(t)601 p, release := newBlockingPTY(t)
602 defer release()602 defer release()
603 d, sess, _, _ := newManualSessionWithPTYs(t, p)603 d, sess, _, _ := newManualSessionWithPTYs(t, p)
604- tb := sess.activeTab()604+ tb := testAttachmentTab(sess)
605 pane := tb.focusedPane()605 pane := tb.focusedPane()
606 606
607 // Holding daemon ownership makes a picker scan block. An active attached607 // Holding daemon ownership makes a picker scan block. An active attached
@@ -632,7 +632,7 @@ func TestNonRenderablePaneDamageRemainsPendingForCapture(t *testing.T) {
632 {name: "inactive tab", setup: func(_ *Daemon, sess *session, tb *tab, _ *pane) {632 {name: "inactive tab", setup: func(_ *Daemon, sess *session, tb *tab, _ *pane) {
633 other := newTab(nil, domain.Size{Cols: 80, Rows: 23})633 other := newTab(nil, domain.Size{Cols: 80, Rows: 23})
634 sess.tabs = append([]*tab{other}, sess.tabs...)634 sess.tabs = append([]*tab{other}, sess.tabs...)
635- sess.active = 0635+ selectTestAttachmentTab(sess, 0)
636 require.NotSame(t, other, tb)636 require.NotSame(t, other, tb)
637 }},637 }},
638 {name: "collapsed", setup: func(_ *Daemon, _ *session, tb *tab, p *pane) {638 {name: "collapsed", setup: func(_ *Daemon, _ *session, tb *tab, p *pane) {
@@ -719,7 +719,7 @@ func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
719 p2, releasePTY2 := newBlockingPTY(t)719 p2, releasePTY2 := newBlockingPTY(t)
720 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)720 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
721 defer releasePTY2()721 defer releasePTY2()
722- sess.active = 0722+ selectTestAttachmentTab(sess, 0)
723 sess.tabs[1].focusedPane().screen.Write([]byte("remaining"))723 sess.tabs[1].focusedPane().screen.Write([]byte("remaining"))
724 724
725 d.sessWg.Add(1)725 d.sessWg.Add(1)
@@ -728,7 +728,7 @@ func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
728 728
729 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)729 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)
730 require.Equal(t, 1, sessionCount(d))730 require.Equal(t, 1, sessionCount(d))
731- require.Equal(t, 0, activeTabIndex(sess))731+ require.Equal(t, 0, testAttachmentTabIndex(sess))
732 f := awaitFrame(t, sends, ports.MsgOutput)732 f := awaitFrame(t, sends, ports.MsgOutput)
733 out, err := ports.UnmarshalOutput(f.Payload)733 out, err := ports.UnmarshalOutput(f.Payload)
734 require.NoError(t, err)734 require.NoError(t, err)
@@ -745,7 +745,7 @@ func TestPTYEOFClosesInactiveNonFinalTabAndRepaintsStatus(t *testing.T) {
745 p2, releasePTY2 := newBlockingPTY(t)745 p2, releasePTY2 := newBlockingPTY(t)
746 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)746 d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
747 defer releasePTY1()747 defer releasePTY1()
748- sess.active = 0748+ selectTestAttachmentTab(sess, 0)
749 sess.tabs[0].focusedPane().screen.Write([]byte("active"))749 sess.tabs[0].focusedPane().screen.Write([]byte("active"))
750 750
751 d.sessWg.Add(1)751 d.sessWg.Add(1)
@@ -754,7 +754,7 @@ func TestPTYEOFClosesInactiveNonFinalTabAndRepaintsStatus(t *testing.T) {
754 754
755 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)755 require.Eventually(t, func() bool { return tabCount(sess) == 1 }, 2*time.Second, 5*time.Millisecond)
756 require.Equal(t, 1, sessionCount(d))756 require.Equal(t, 1, sessionCount(d))
757- require.Equal(t, 0, activeTabIndex(sess))757+ require.Equal(t, 0, testAttachmentTabIndex(sess))
758 f := awaitFrame(t, sends, ports.MsgOutput)758 f := awaitFrame(t, sends, ports.MsgOutput)
759 out, err := ports.UnmarshalOutput(f.Payload)759 out, err := ports.UnmarshalOutput(f.Payload)
760 require.NoError(t, err)760 require.NoError(t, err)
@@ -1198,7 +1198,7 @@ func TestPaintAlignsFloatingCursorWithCommittedGeometry(t *testing.T) {
1198 floating := newPane("floating", nil, rectSize(committed.Inner))1198 floating := newPane("floating", nil, rectSize(committed.Inner))
1199 floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'})1199 floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'})
1200 floating.popupGeometry = committed1200 floating.popupGeometry = committed
1201- installTestFloating(sess.activeTab(), floating, true)1201+ installTestFloating(testAttachmentTab(sess), floating, true)
1202 1202
1203 d.paint(sess, ac, true, nil)1203 d.paint(sess, ac, true, nil)
1204 data := mustOutputData(t, sends)1204 data := mustOutputData(t, sends)
@@ -1300,7 +1300,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) {
1300 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})1300 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})
1301 appendHistoryRow(t, fp.history, testRow("flt-old"))1301 appendHistoryRow(t, fp.history, testRow("flt-old"))
1302 fp.screen.Write([]byte("flt-live"))1302 fp.screen.Write([]byte("flt-live"))
1303- installTestFloating(sess.activeTab(), fp, true)1303+ installTestFloating(testAttachmentTab(sess), fp, true)
1304 1304
1305 d.enterCopyMode(sess, ac)1305 d.enterCopyMode(sess, ac)
1306 data := mustOutputData(t, sends)1306 data := mustOutputData(t, sends)
M internal/usecase/daemon/resize_mode_test.go +7 -7
@@ -64,7 +64,7 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
6464 require.Equal(t, "pane is not in a split", tooEarly.Text)
6565
6666 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
67- tb := sess.activeTab()
67+ tb := testAttachmentTab(sess)
6868 tb.mu.Lock()
6969 beforeFocus := tb.tree.Focus
7070 beforeGeneration := tb.layoutGeneration
@@ -130,7 +130,7 @@ func TestResizeModeStateRenderingAndMouseConsumption(t *testing.T) {
130130 snapshot.Unlock()
131131 require.False(t, (capturedOverlayRenderState{resizeActive: true}).active(), "resize must not create a modal render layer")
132132
133- tb := sess.activeTab()
133+ tb := testAttachmentTab(sess)
134134 tb.mu.Lock()
135135 focused := tb.focusedPane()
136136 tb.mu.Unlock()
@@ -199,7 +199,7 @@ func TestResizeModeKeysAndEscapes(t *testing.T) {
199199 for _, tt := range tests {
200200 t.Run(tt.name, func(t *testing.T) {
201201 d, sess, ac := resizeModeFixture(t)
202- tb := sess.activeTab()
202+ tb := testAttachmentTab(sess)
203203 tb.mu.Lock()
204204 before := tb.layoutGeneration
205205 if tt.equalize {
@@ -229,7 +229,7 @@ func TestResizeModeKeysAndEscapes(t *testing.T) {
229229
230230 func TestResizeModeRendersGuidanceWithoutHidingContentOrCursor(t *testing.T) {
231231 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
232- tb := sess.activeTab()
232+ tb := testAttachmentTab(sess)
233233 second := newPane("pane-2", nil, domain.Size{Cols: 39, Rows: 23})
234234 tb.mu.Lock()
235235 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -296,7 +296,7 @@ func TestResizeModeOverlayPriority(t *testing.T) {
296296 func TestResizeModeWarningPersistsAndActionsShareTransaction(t *testing.T) {
297297 t.Run("too small warning leaves mode active", func(t *testing.T) {
298298 d, sess, ac := resizeModeFixture(t)
299- tb := sess.activeTab()
299+ tb := testAttachmentTab(sess)
300300 tb.mu.Lock()
301301 tb.size.Cols = 41 // two minimum-width panes and their separator
302302 tb.bumpLayoutGenerationLocked()
@@ -331,7 +331,7 @@ func TestResizeModeWarningPersistsAndActionsShareTransaction(t *testing.T) {
331331 t.Run(tt.name, func(t *testing.T) {
332332 d, sess, ac := resizeModeFixture(t)
333333 require.NoError(t, tt.run(d, sess, ac))
334- tb := sess.activeTab()
334+ tb := testAttachmentTab(sess)
335335 tb.mu.Lock()
336336 got := []float64{tb.tree.Root.Children[0].Weight, tb.tree.Root.Children[1].Weight}
337337 tb.mu.Unlock()
@@ -372,7 +372,7 @@ func TestResizeModeReleasesTabAndOverlayLocksBeforePTY(t *testing.T) {
372372 // Build a live tree directly so the probe is not concurrently owned by a
373373 // pane reader; this isolates the lock boundary being asserted.
374374 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
375- tb := sess.activeTab()
375+ tb := testAttachmentTab(sess)
376376 probe := &resizeLockProbePTY{entered: make(chan struct{}), release: make(chan struct{})}
377377 second := newPane("pane-2", probe, domain.Size{Cols: 39, Rows: 23})
378378 tb.mu.Lock()
@@ -64,7 +64,7 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
64 require.Equal(t, "pane is not in a split", tooEarly.Text)64 require.Equal(t, "pane is not in a split", tooEarly.Text)
65 65
66 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)66 require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
67- tb := sess.activeTab()67+ tb := testAttachmentTab(sess)
68 tb.mu.Lock()68 tb.mu.Lock()
69 beforeFocus := tb.tree.Focus69 beforeFocus := tb.tree.Focus
70 beforeGeneration := tb.layoutGeneration70 beforeGeneration := tb.layoutGeneration
@@ -130,7 +130,7 @@ func TestResizeModeStateRenderingAndMouseConsumption(t *testing.T) {
130 snapshot.Unlock()130 snapshot.Unlock()
131 require.False(t, (capturedOverlayRenderState{resizeActive: true}).active(), "resize must not create a modal render layer")131 require.False(t, (capturedOverlayRenderState{resizeActive: true}).active(), "resize must not create a modal render layer")
132 132
133- tb := sess.activeTab()133+ tb := testAttachmentTab(sess)
134 tb.mu.Lock()134 tb.mu.Lock()
135 focused := tb.focusedPane()135 focused := tb.focusedPane()
136 tb.mu.Unlock()136 tb.mu.Unlock()
@@ -199,7 +199,7 @@ func TestResizeModeKeysAndEscapes(t *testing.T) {
199 for _, tt := range tests {199 for _, tt := range tests {
200 t.Run(tt.name, func(t *testing.T) {200 t.Run(tt.name, func(t *testing.T) {
201 d, sess, ac := resizeModeFixture(t)201 d, sess, ac := resizeModeFixture(t)
202- tb := sess.activeTab()202+ tb := testAttachmentTab(sess)
203 tb.mu.Lock()203 tb.mu.Lock()
204 before := tb.layoutGeneration204 before := tb.layoutGeneration
205 if tt.equalize {205 if tt.equalize {
@@ -229,7 +229,7 @@ func TestResizeModeKeysAndEscapes(t *testing.T) {
229 229
230 func TestResizeModeRendersGuidanceWithoutHidingContentOrCursor(t *testing.T) {230 func TestResizeModeRendersGuidanceWithoutHidingContentOrCursor(t *testing.T) {
231 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)231 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
232- tb := sess.activeTab()232+ tb := testAttachmentTab(sess)
233 second := newPane("pane-2", nil, domain.Size{Cols: 39, Rows: 23})233 second := newPane("pane-2", nil, domain.Size{Cols: 39, Rows: 23})
234 tb.mu.Lock()234 tb.mu.Lock()
235 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}235 tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"}
@@ -296,7 +296,7 @@ func TestResizeModeOverlayPriority(t *testing.T) {
296 func TestResizeModeWarningPersistsAndActionsShareTransaction(t *testing.T) {296 func TestResizeModeWarningPersistsAndActionsShareTransaction(t *testing.T) {
297 t.Run("too small warning leaves mode active", func(t *testing.T) {297 t.Run("too small warning leaves mode active", func(t *testing.T) {
298 d, sess, ac := resizeModeFixture(t)298 d, sess, ac := resizeModeFixture(t)
299- tb := sess.activeTab()299+ tb := testAttachmentTab(sess)
300 tb.mu.Lock()300 tb.mu.Lock()
301 tb.size.Cols = 41 // two minimum-width panes and their separator301 tb.size.Cols = 41 // two minimum-width panes and their separator
302 tb.bumpLayoutGenerationLocked()302 tb.bumpLayoutGenerationLocked()
@@ -331,7 +331,7 @@ func TestResizeModeWarningPersistsAndActionsShareTransaction(t *testing.T) {
331 t.Run(tt.name, func(t *testing.T) {331 t.Run(tt.name, func(t *testing.T) {
332 d, sess, ac := resizeModeFixture(t)332 d, sess, ac := resizeModeFixture(t)
333 require.NoError(t, tt.run(d, sess, ac))333 require.NoError(t, tt.run(d, sess, ac))
334- tb := sess.activeTab()334+ tb := testAttachmentTab(sess)
335 tb.mu.Lock()335 tb.mu.Lock()
336 got := []float64{tb.tree.Root.Children[0].Weight, tb.tree.Root.Children[1].Weight}336 got := []float64{tb.tree.Root.Children[0].Weight, tb.tree.Root.Children[1].Weight}
337 tb.mu.Unlock()337 tb.mu.Unlock()
@@ -372,7 +372,7 @@ func TestResizeModeReleasesTabAndOverlayLocksBeforePTY(t *testing.T) {
372 // Build a live tree directly so the probe is not concurrently owned by a372 // Build a live tree directly so the probe is not concurrently owned by a
373 // pane reader; this isolates the lock boundary being asserted.373 // pane reader; this isolates the lock boundary being asserted.
374 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)374 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
375- tb := sess.activeTab()375+ tb := testAttachmentTab(sess)
376 probe := &resizeLockProbePTY{entered: make(chan struct{}), release: make(chan struct{})}376 probe := &resizeLockProbePTY{entered: make(chan struct{}), release: make(chan struct{})}
377 second := newPane("pane-2", probe, domain.Size{Cols: 39, Rows: 23})377 second := newPane("pane-2", probe, domain.Size{Cols: 39, Rows: 23})
378 tb.mu.Lock()378 tb.mu.Lock()
M internal/usecase/daemon/role_effect_gate_test.go +5 -5
@@ -626,7 +626,7 @@ func TestRoleEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *testing.T
626626 {
627627 name: "mouse input",
628628 setup: func(_ *Daemon, sess *session, _ *attachedClient, _ *transactionalResizePTY) {
629- p := sess.activeTab().focusedPane()
629+ p := testAttachmentTab(sess).focusedPane()
630630 p.mu.Lock()
631631 p.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
632632 p.mu.Unlock()
@@ -769,12 +769,12 @@ func TestJumpAttentionDoesNotMutateSourceAfterInitiatorReplacement(t *testing.T)
769769 })
770770 require.NoError(t, err)
771771 sess.mu.Lock()
772- sess.active = 0
772+ selectTestAttachmentTabLocked(sess, 0)
773773 sess.mu.Unlock()
774774 release()
775775 <-actionDone
776776
777- require.Equal(t, 0, activeTabIndex(sess), "the replaced initiator mutated source focus after losing its role")
777+ require.Equal(t, 0, testAttachmentTabIndex(sess), "the replaced initiator mutated source focus after losing its role")
778778 d.deferAttachmentTransitionCleanups(result)
779779 }
780780
@@ -802,7 +802,7 @@ func TestJumpAttentionAdmittedHandoffCrossesSessions(t *testing.T) {
802802 d.handleActiveClientFrame(token, frameInput([]byte("\x1ba")))
803803
804804 require.Same(t, target, ac.currentSession())
805- require.Equal(t, 1, activeTabIndex(target))
805+ require.Equal(t, 1, testAttachmentTabIndex(target))
806806 }
807807
808808 func TestJumpAttentionHandoffRevalidatesInitiatorAfterAdmissionEnds(t *testing.T) {
@@ -854,7 +854,7 @@ func TestJumpAttentionHandoffRevalidatesInitiatorAfterAdmissionEnds(t *testing.T
854854 release()
855855 <-actionDone
856856
857- require.Equal(t, 0, activeTabIndex(target), "stale handoff changed target focus")
857+ require.Equal(t, 0, testAttachmentTabIndex(target), "stale handoff changed target focus")
858858 target.mu.Lock()
859859 targetAttachments := target.snapshotAttachmentsLocked()
860860 target.mu.Unlock()
@@ -626,7 +626,7 @@ func TestRoleEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *testing.T
626 {626 {
627 name: "mouse input",627 name: "mouse input",
628 setup: func(_ *Daemon, sess *session, _ *attachedClient, _ *transactionalResizePTY) {628 setup: func(_ *Daemon, sess *session, _ *attachedClient, _ *transactionalResizePTY) {
629- p := sess.activeTab().focusedPane()629+ p := testAttachmentTab(sess).focusedPane()
630 p.mu.Lock()630 p.mu.Lock()
631 p.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))631 p.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
632 p.mu.Unlock()632 p.mu.Unlock()
@@ -769,12 +769,12 @@ func TestJumpAttentionDoesNotMutateSourceAfterInitiatorReplacement(t *testing.T)
769 })769 })
770 require.NoError(t, err)770 require.NoError(t, err)
771 sess.mu.Lock()771 sess.mu.Lock()
772- sess.active = 0772+ selectTestAttachmentTabLocked(sess, 0)
773 sess.mu.Unlock()773 sess.mu.Unlock()
774 release()774 release()
775 <-actionDone775 <-actionDone
776 776
777- require.Equal(t, 0, activeTabIndex(sess), "the replaced initiator mutated source focus after losing its role")777+ require.Equal(t, 0, testAttachmentTabIndex(sess), "the replaced initiator mutated source focus after losing its role")
778 d.deferAttachmentTransitionCleanups(result)778 d.deferAttachmentTransitionCleanups(result)
779 }779 }
780 780
@@ -802,7 +802,7 @@ func TestJumpAttentionAdmittedHandoffCrossesSessions(t *testing.T) {
802 d.handleActiveClientFrame(token, frameInput([]byte("\x1ba")))802 d.handleActiveClientFrame(token, frameInput([]byte("\x1ba")))
803 803
804 require.Same(t, target, ac.currentSession())804 require.Same(t, target, ac.currentSession())
805- require.Equal(t, 1, activeTabIndex(target))805+ require.Equal(t, 1, testAttachmentTabIndex(target))
806 }806 }
807 807
808 func TestJumpAttentionHandoffRevalidatesInitiatorAfterAdmissionEnds(t *testing.T) {808 func TestJumpAttentionHandoffRevalidatesInitiatorAfterAdmissionEnds(t *testing.T) {
@@ -854,7 +854,7 @@ func TestJumpAttentionHandoffRevalidatesInitiatorAfterAdmissionEnds(t *testing.T
854 release()854 release()
855 <-actionDone855 <-actionDone
856 856
857- require.Equal(t, 0, activeTabIndex(target), "stale handoff changed target focus")857+ require.Equal(t, 0, testAttachmentTabIndex(target), "stale handoff changed target focus")
858 target.mu.Lock()858 target.mu.Lock()
859 targetAttachments := target.snapshotAttachmentsLocked()859 targetAttachments := target.snapshotAttachmentsLocked()
860 target.mu.Unlock()860 target.mu.Unlock()
M internal/usecase/daemon/session.go +3 -25
@@ -51,14 +51,13 @@ type session struct {
5151 teardownWaiters uint
5252 teardownChanged chan struct{}
5353 lifecycleStopOnce sync.Once
54- // sessionCore.mu guards tabs, active, attachment membership, restoreDone,
55- // clipFiles, and clipboard queue state.
54+ // sessionCore.mu guards tabs, attachment membership, restoreDone, clipFiles,
55+ // and clipboard queue state.
5656 // restoreDone remains attached to a restored session after it is published in
5757 // the live registry, so racing attaches cannot bypass restoration completion.
5858 restoreDone chan struct{}
5959 themeMu sync.Mutex
6060 tabs []*tab
61- active int
6261 clipboardQueue []clipboardForward
6362 clipboardWorkerRunning bool
6463 cwd string
@@ -854,27 +853,6 @@ func (d *Daemon) detachIfRoleCurrentUntil(token attachmentRoleToken, done func()
854853 return current
855854 }
856855
857-func (s *session) activeTab() *tab {
858- s.mu.Lock()
859- defer s.mu.Unlock()
860- if s.active < 0 || s.active >= len(s.tabs) {
861- return nil
862- }
863- return s.tabs[s.active]
864-}
865-
866-func (s *session) switchTab(idx int) bool {
867- s.mu.Lock()
868- if idx < 0 || idx >= len(s.tabs) || idx == s.active {
869- s.mu.Unlock()
870- return false
871- }
872- s.active = idx
873- s.mu.Unlock()
874- markSnapshotDirty(s)
875- return true
876-}
877-
878856 func (d *Daemon) renameSession(sess *session, name string) error {
879857 if name == "" {
880858 return errSessionNameRequired
@@ -1747,7 +1725,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
17471725 if d.procCwd == nil {
17481726 return
17491727 }
1750- tb := sess.tabForAttachmentOrActive(nil)
1728+ tb := sess.firstTab()
17511729 if tb == nil {
17521730 return
17531731 }
@@ -51,14 +51,13 @@ type session struct {
51 teardownWaiters uint51 teardownWaiters uint
52 teardownChanged chan struct{}52 teardownChanged chan struct{}
53 lifecycleStopOnce sync.Once53 lifecycleStopOnce sync.Once
54- // sessionCore.mu guards tabs, active, attachment membership, restoreDone,54+ // sessionCore.mu guards tabs, attachment membership, restoreDone, clipFiles,
55- // clipFiles, and clipboard queue state.55+ // and clipboard queue state.
56 // restoreDone remains attached to a restored session after it is published in56 // restoreDone remains attached to a restored session after it is published in
57 // the live registry, so racing attaches cannot bypass restoration completion.57 // the live registry, so racing attaches cannot bypass restoration completion.
58 restoreDone chan struct{}58 restoreDone chan struct{}
59 themeMu sync.Mutex59 themeMu sync.Mutex
60 tabs []*tab60 tabs []*tab
61- active int
62 clipboardQueue []clipboardForward61 clipboardQueue []clipboardForward
63 clipboardWorkerRunning bool62 clipboardWorkerRunning bool
64 cwd string63 cwd string
@@ -854,27 +853,6 @@ func (d *Daemon) detachIfRoleCurrentUntil(token attachmentRoleToken, done func()
854 return current853 return current
855 }854 }
856 855
857-func (s *session) activeTab() *tab {
858- s.mu.Lock()
859- defer s.mu.Unlock()
860- if s.active < 0 || s.active >= len(s.tabs) {
861- return nil
862- }
863- return s.tabs[s.active]
864-}
865-
866-func (s *session) switchTab(idx int) bool {
867- s.mu.Lock()
868- if idx < 0 || idx >= len(s.tabs) || idx == s.active {
869- s.mu.Unlock()
870- return false
871- }
872- s.active = idx
873- s.mu.Unlock()
874- markSnapshotDirty(s)
875- return true
876-}
877-
878 func (d *Daemon) renameSession(sess *session, name string) error {856 func (d *Daemon) renameSession(sess *session, name string) error {
879 if name == "" {857 if name == "" {
880 return errSessionNameRequired858 return errSessionNameRequired
@@ -1747,7 +1725,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
1747 if d.procCwd == nil {1725 if d.procCwd == nil {
1748 return1726 return
1749 }1727 }
1750- tb := sess.tabForAttachmentOrActive(nil)1728+ tb := sess.firstTab()
1751 if tb == nil {1729 if tb == nil {
1752 return1730 return
1753 }1731 }
M internal/usecase/daemon/session_core.go +5 -4
@@ -14,7 +14,7 @@ type attachmentSession interface {
1414 snapshotView(viewOptions) sessionView
1515 statusSegments(includeTerminalTitle bool) statusSnapshot
1616 capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool)
17- activateTargetLocked(tabIndex int) bool
17+ validTargetTabLocked(tabIndex int) bool
1818 isProxy() bool
1919 }
2020
@@ -53,9 +53,10 @@ func (s *session) capturePrimary(ac *attachedClient, req primaryCaptureRequest)
5353 return captureLocalPrimaryRenderState(s, ac, req)
5454 }
5555
56-// activateTargetLocked validates a legacy session target. Client-facing
57-// navigation updates attachment views through selectAttachmentTab.
58-func (s *session) activateTargetLocked(tabIndex int) bool {
56+// validTargetTabLocked validates a target tab index without changing any
57+// attachment view. Client-facing navigation updates attachment views through
58+// selectAttachmentTab.
59+func (s *session) validTargetTabLocked(tabIndex int) bool {
5960 if tabIndex < 0 {
6061 return true
6162 }
@@ -14,7 +14,7 @@ type attachmentSession interface {
14 snapshotView(viewOptions) sessionView14 snapshotView(viewOptions) sessionView
15 statusSegments(includeTerminalTitle bool) statusSnapshot15 statusSegments(includeTerminalTitle bool) statusSnapshot
16 capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool)16 capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool)
17- activateTargetLocked(tabIndex int) bool17+ validTargetTabLocked(tabIndex int) bool
18 isProxy() bool18 isProxy() bool
19 }19 }
20 20
@@ -53,9 +53,10 @@ func (s *session) capturePrimary(ac *attachedClient, req primaryCaptureRequest)
53 return captureLocalPrimaryRenderState(s, ac, req)53 return captureLocalPrimaryRenderState(s, ac, req)
54 }54 }
55 55
56-// activateTargetLocked validates a legacy session target. Client-facing56+// validTargetTabLocked validates a target tab index without changing any
57-// navigation updates attachment views through selectAttachmentTab.57+// attachment view. Client-facing navigation updates attachment views through
58-func (s *session) activateTargetLocked(tabIndex int) bool {58+// selectAttachmentTab.
59+func (s *session) validTargetTabLocked(tabIndex int) bool {
59 if tabIndex < 0 {60 if tabIndex < 0 {
60 return true61 return true
61 }62 }
M internal/usecase/daemon/session_core_test.go +7 -5
@@ -49,7 +49,7 @@ func TestSessionCorePreservesLocalIdentityAndPromotedMutex(t *testing.T) {
4949 func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.T) {
5050 sess := &session{
5151 sessionCore: sessionCore{id: domain.SessionID("local"), name: "work"},
52- tabs: []*tab{{name: "shell"}},
52+ tabs: []*tab{{stableID: "tab-shell", name: "shell"}},
5353 }
5454 ac := &attachedClient{}
5555
@@ -57,9 +57,9 @@ func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.
5757 // promoted core mutex. snapshotView must observe the same mutex before it
5858 // can publish either field.
5959 sess.core().mu.Lock()
60- sess.tabs = append(sess.tabs, &tab{name: "logs"})
61- sess.active = 1
60+ sess.tabs = append(sess.tabs, &tab{stableID: "tab-logs", name: "logs"})
6261 sess.registerAttachmentLocked(ac)
62+ sess.activateAttachmentViewLocked(ac, 1)
6363
6464 preCall := make(chan struct{})
6565 allowCall := make(chan struct{})
@@ -89,7 +89,8 @@ func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.
8989 view := awaitTestValue(t, snapshotted, "snapshotView did not complete after sessionCore.mu was released")
9090 require.True(t, view.attached)
9191 require.Equal(t, 2, view.tabCount)
92- require.Equal(t, 1, view.active)
92+ require.Equal(t, 0, view.defaultTab)
93+ require.Equal(t, 1, testAttachmentTabIndex(sess))
9394 }
9495
9596 func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {
@@ -165,8 +166,9 @@ func TestInitialMetadataSkipsInvalidSnapshot(t *testing.T) {
165166 sess := firstSession(d)
166167 require.NotNil(t, sess)
167168 sess.mu.Lock()
168- sess.active = len(sess.tabs)
169+ invalidTabIndex := len(sess.tabs)
169170 sess.mu.Unlock()
171+ selectTestAttachmentTab(sess, invalidTabIndex)
170172 // finish (not just release) also closes recvDone, so runConnLoop's Recv
171173 // fails immediately after the first paint and the handshake goroutine
172174 // returns instead of blocking on further input.
@@ -49,7 +49,7 @@ func TestSessionCorePreservesLocalIdentityAndPromotedMutex(t *testing.T) {
49 func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.T) {49 func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.T) {
50 sess := &session{50 sess := &session{
51 sessionCore: sessionCore{id: domain.SessionID("local"), name: "work"},51 sessionCore: sessionCore{id: domain.SessionID("local"), name: "work"},
52- tabs: []*tab{{name: "shell"}},52+ tabs: []*tab{{stableID: "tab-shell", name: "shell"}},
53 }53 }
54 ac := &attachedClient{}54 ac := &attachedClient{}
55 55
@@ -57,9 +57,9 @@ func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.
57 // promoted core mutex. snapshotView must observe the same mutex before it57 // promoted core mutex. snapshotView must observe the same mutex before it
58 // can publish either field.58 // can publish either field.
59 sess.core().mu.Lock()59 sess.core().mu.Lock()
60- sess.tabs = append(sess.tabs, &tab{name: "logs"})60+ sess.tabs = append(sess.tabs, &tab{stableID: "tab-logs", name: "logs"})
61- sess.active = 1
62 sess.registerAttachmentLocked(ac)61 sess.registerAttachmentLocked(ac)
62+ sess.activateAttachmentViewLocked(ac, 1)
63 63
64 preCall := make(chan struct{})64 preCall := make(chan struct{})
65 allowCall := make(chan struct{})65 allowCall := make(chan struct{})
@@ -89,7 +89,8 @@ func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.
89 view := awaitTestValue(t, snapshotted, "snapshotView did not complete after sessionCore.mu was released")89 view := awaitTestValue(t, snapshotted, "snapshotView did not complete after sessionCore.mu was released")
90 require.True(t, view.attached)90 require.True(t, view.attached)
91 require.Equal(t, 2, view.tabCount)91 require.Equal(t, 2, view.tabCount)
92- require.Equal(t, 1, view.active)92+ require.Equal(t, 0, view.defaultTab)
93+ require.Equal(t, 1, testAttachmentTabIndex(sess))
93 }94 }
94 95
95 func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {96 func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {
@@ -165,8 +166,9 @@ func TestInitialMetadataSkipsInvalidSnapshot(t *testing.T) {
165 sess := firstSession(d)166 sess := firstSession(d)
166 require.NotNil(t, sess)167 require.NotNil(t, sess)
167 sess.mu.Lock()168 sess.mu.Lock()
168- sess.active = len(sess.tabs)169+ invalidTabIndex := len(sess.tabs)
169 sess.mu.Unlock()170 sess.mu.Unlock()
171+ selectTestAttachmentTab(sess, invalidTabIndex)
170 // finish (not just release) also closes recvDone, so runConnLoop's Recv172 // finish (not just release) also closes recvDone, so runConnLoop's Recv
171 // fails immediately after the first paint and the handshake goroutine173 // fails immediately after the first paint and the handshake goroutine
172 // returns instead of blocking on further input.174 // returns instead of blocking on further input.
M internal/usecase/daemon/session_size_test.go +3 -3
@@ -14,12 +14,12 @@ func TestFullViewportSize(t *testing.T) {
1414 }{
1515 {
1616 name: "derives from active tab content size",
17- sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}, active: 0},
17+ sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}},
1818 want: domain.Size{Cols: 120, Rows: 40},
1919 },
2020 {
2121 name: "round-trips through tabSize",
22- sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}, active: 0},
22+ sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}},
2323 want: domain.Size{Cols: 80, Rows: 24},
2424 },
2525 {
@@ -29,7 +29,7 @@ func TestFullViewportSize(t *testing.T) {
2929 },
3030 {
3131 name: "falls back to defaultSize with invalid tab size",
32- sess: &session{tabs: []*tab{{}}, active: 0},
32+ sess: &session{tabs: []*tab{{}}},
3333 want: defaultSize,
3434 },
3535 }
@@ -14,12 +14,12 @@ func TestFullViewportSize(t *testing.T) {
14 }{14 }{
15 {15 {
16 name: "derives from active tab content size",16 name: "derives from active tab content size",
17- sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}, active: 0},17+ sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}},
18 want: domain.Size{Cols: 120, Rows: 40},18 want: domain.Size{Cols: 120, Rows: 40},
19 },19 },
20 {20 {
21 name: "round-trips through tabSize",21 name: "round-trips through tabSize",
22- sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}, active: 0},22+ sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}},
23 want: domain.Size{Cols: 80, Rows: 24},23 want: domain.Size{Cols: 80, Rows: 24},
24 },24 },
25 {25 {
@@ -29,7 +29,7 @@ func TestFullViewportSize(t *testing.T) {
29 },29 },
30 {30 {
31 name: "falls back to defaultSize with invalid tab size",31 name: "falls back to defaultSize with invalid tab size",
32- sess: &session{tabs: []*tab{{}}, active: 0},32+ sess: &session{tabs: []*tab{{}}},
33 want: defaultSize,33 want: defaultSize,
34 },34 },
35 }35 }
M internal/usecase/daemon/session_test.go +3 -3
@@ -72,7 +72,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) {
7272
7373 require.NoError(t, d.createTab(sess, sz))
7474 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)
75- requireFloatingInitialized(t, sess.activeTab())
75+ requireFloatingInitialized(t, testAttachmentTab(sess))
7676 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
7777 releaseFirst()
7878 releaseSecond()
@@ -998,7 +998,7 @@ func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
998998 defer releases[1]()
999999 sess.mu.Lock()
10001000 first, closing := sess.tabs[0], sess.tabs[1]
1001- sess.active = 1
1001+ selectTestAttachmentTabLocked(sess, 1)
10021002 sess.mu.Unlock()
10031003 first.mu.Lock()
10041004 stale := first.takeFloatingLocked()
@@ -1007,7 +1007,7 @@ func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
10071007
10081008 require.NoError(t, d.closeTab(sess, closing, false))
10091009
1010- require.Same(t, first, sess.activeTab())
1010+ require.Same(t, first, testAttachmentTab(sess))
10111011 requireFloatingInitialized(t, first)
10121012 }
10131013
@@ -72,7 +72,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) {
72 72
73 require.NoError(t, d.createTab(sess, sz))73 require.NoError(t, d.createTab(sess, sz))
74 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)74 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)
75- requireFloatingInitialized(t, sess.activeTab())75+ requireFloatingInitialized(t, testAttachmentTab(sess))
76 _ = d.killSession(sess, ports.ReasonSessionKilled, false)76 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
77 releaseFirst()77 releaseFirst()
78 releaseSecond()78 releaseSecond()
@@ -998,7 +998,7 @@ func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
998 defer releases[1]()998 defer releases[1]()
999 sess.mu.Lock()999 sess.mu.Lock()
1000 first, closing := sess.tabs[0], sess.tabs[1]1000 first, closing := sess.tabs[0], sess.tabs[1]
1001- sess.active = 11001+ selectTestAttachmentTabLocked(sess, 1)
1002 sess.mu.Unlock()1002 sess.mu.Unlock()
1003 first.mu.Lock()1003 first.mu.Lock()
1004 stale := first.takeFloatingLocked()1004 stale := first.takeFloatingLocked()
@@ -1007,7 +1007,7 @@ func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
1007 1007
1008 require.NoError(t, d.closeTab(sess, closing, false))1008 require.NoError(t, d.closeTab(sess, closing, false))
1009 1009
1010- require.Same(t, first, sess.activeTab())1010+ require.Same(t, first, testAttachmentTab(sess))
1011 requireFloatingInitialized(t, first)1011 requireFloatingInitialized(t, first)
1012 }1012 }
1013 1013
M internal/usecase/daemon/session_view.go +6 -14
@@ -34,7 +34,7 @@ type sessionView struct {
3434 name string
3535 ephemeral bool
3636 createdAt int64
37- active int
37+ defaultTab int
3838 mruAt uint64
3939 attached bool
4040 tabCount int
@@ -53,24 +53,16 @@ func (s *session) snapshotView(opts viewOptions) sessionView {
5353 opts.tabDetails = true
5454 }
5555 s.mu.Lock()
56- active := 0
57- if len(s.attachments) != 0 {
58- for i, tb := range s.tabs {
59- for ac := range s.attachments {
60- if ac.viewSnapshot().tabID == domain.TabStableID(tb.stableID) {
61- active = i
62- break
63- }
64- }
65- }
66- }
56+ // Session-level listings use the first ordered tab as their deterministic
57+ // default. Interactive callers pass an attachment and resolve its stable
58+ // view separately.
6759 view := sessionView{
6860 id: s.id,
6961 incarnation: s.incarnation,
7062 name: s.name,
7163 ephemeral: s.ephemeral,
7264 createdAt: s.createdAt,
73- active: active,
65+ defaultTab: 0,
7466 mruAt: s.mruAt.Load(),
7567 attached: len(s.attachments) != 0,
7668 tabCount: len(s.tabs),
@@ -115,7 +107,7 @@ func (view sessionView) pickerView() picker.SessionView {
115107 Incarnation: view.incarnation,
116108 Name: view.name,
117109 TargetName: view.name,
118- Active: view.active,
110+ Active: view.defaultTab,
119111 Tabs: make([]picker.TabEntry, 0, len(view.tabs)),
120112 CannotAcceptMoves: view.cannotAcceptMoves,
121113 }
@@ -34,7 +34,7 @@ type sessionView struct {
34 name string34 name string
35 ephemeral bool35 ephemeral bool
36 createdAt int6436 createdAt int64
37- active int37+ defaultTab int
38 mruAt uint6438 mruAt uint64
39 attached bool39 attached bool
40 tabCount int40 tabCount int
@@ -53,24 +53,16 @@ func (s *session) snapshotView(opts viewOptions) sessionView {
53 opts.tabDetails = true53 opts.tabDetails = true
54 }54 }
55 s.mu.Lock()55 s.mu.Lock()
56- active := 056+ // Session-level listings use the first ordered tab as their deterministic
57- if len(s.attachments) != 0 {57+ // default. Interactive callers pass an attachment and resolve its stable
58- for i, tb := range s.tabs {58+ // view separately.
59- for ac := range s.attachments {
60- if ac.viewSnapshot().tabID == domain.TabStableID(tb.stableID) {
61- active = i
62- break
63- }
64- }
65- }
66- }
67 view := sessionView{59 view := sessionView{
68 id: s.id,60 id: s.id,
69 incarnation: s.incarnation,61 incarnation: s.incarnation,
70 name: s.name,62 name: s.name,
71 ephemeral: s.ephemeral,63 ephemeral: s.ephemeral,
72 createdAt: s.createdAt,64 createdAt: s.createdAt,
73- active: active,65+ defaultTab: 0,
74 mruAt: s.mruAt.Load(),66 mruAt: s.mruAt.Load(),
75 attached: len(s.attachments) != 0,67 attached: len(s.attachments) != 0,
76 tabCount: len(s.tabs),68 tabCount: len(s.tabs),
@@ -115,7 +107,7 @@ func (view sessionView) pickerView() picker.SessionView {
115 Incarnation: view.incarnation,107 Incarnation: view.incarnation,
116 Name: view.name,108 Name: view.name,
117 TargetName: view.name,109 TargetName: view.name,
118- Active: view.active,110+ Active: view.defaultTab,
119 Tabs: make([]picker.TabEntry, 0, len(view.tabs)),111 Tabs: make([]picker.TabEntry, 0, len(view.tabs)),
120 CannotAcceptMoves: view.cannotAcceptMoves,112 CannotAcceptMoves: view.cannotAcceptMoves,
121 }113 }
M internal/usecase/daemon/session_view_test.go +1 -2
@@ -22,7 +22,7 @@ func TestSnapshotViewCapturesSessionFields(t *testing.T) {
2222 name: "alpha",
2323 createdAt: 42,
2424
25- attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, active: 1,
25+ attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}},
2626
2727 tabs: []*tab{
2828 {stableID: "t1", name: "build"},
@@ -38,7 +38,6 @@ func TestSnapshotViewCapturesSessionFields(t *testing.T) {
3838 incarnation: domain.IncarnationID{1},
3939 name: "alpha",
4040 createdAt: 42,
41- active: 1,
4241 mruAt: 7,
4342 attached: true,
4443 tabCount: 2,
@@ -22,7 +22,7 @@ func TestSnapshotViewCapturesSessionFields(t *testing.T) {
22 name: "alpha",22 name: "alpha",
23 createdAt: 42,23 createdAt: 42,
24 24
25- attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}}, active: 1,25+ attachments: map[*attachedClient]struct{}{&attachedClient{}: {}}},
26 26
27 tabs: []*tab{27 tabs: []*tab{
28 {stableID: "t1", name: "build"},28 {stableID: "t1", name: "build"},
@@ -38,7 +38,6 @@ func TestSnapshotViewCapturesSessionFields(t *testing.T) {
38 incarnation: domain.IncarnationID{1},38 incarnation: domain.IncarnationID{1},
39 name: "alpha",39 name: "alpha",
40 createdAt: 42,40 createdAt: 42,
41- active: 1,
42 mruAt: 7,41 mruAt: 7,
43 attached: true,42 attached: true,
44 tabCount: 2,43 tabCount: 2,
M internal/usecase/daemon/snapshot.go +1 -1
@@ -42,7 +42,7 @@ type snapshotCapture struct {
4242 name string
4343 incarnation domain.IncarnationID
4444 createdAt uint64
45- active uint16
45+ defaultTab uint16
4646 tabs []snapshotCaptureTab
4747 publicationContext context.Context
4848 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker
@@ -42,7 +42,7 @@ type snapshotCapture struct {
42 name string42 name string
43 incarnation domain.IncarnationID43 incarnation domain.IncarnationID
44 createdAt uint6444 createdAt uint64
45- active uint1645+ defaultTab uint16
46 tabs []snapshotCaptureTab46 tabs []snapshotCaptureTab
47 publicationContext context.Context47 publicationContext context.Context
48 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker48 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker
M internal/usecase/daemon/snapshot_capture.go +3 -1
@@ -13,7 +13,9 @@ func (d *Daemon) captureSnapshotState(sess *session, generation uint64) (*snapsh
1313 name: sess.name,
1414 incarnation: sess.incarnation,
1515 createdAt: uint64(sess.createdAt),
16- active: 0,
16+ // Snapshot format retains a deterministic first-tab default; it is not
17+ // an interactive attachment selection.
18+ defaultTab: 0,
1719 }
1820 ephemeral := sess.ephemeral
1921 fallbackCwd := sess.cwd
@@ -13,7 +13,9 @@ func (d *Daemon) captureSnapshotState(sess *session, generation uint64) (*snapsh
13 name: sess.name,13 name: sess.name,
14 incarnation: sess.incarnation,14 incarnation: sess.incarnation,
15 createdAt: uint64(sess.createdAt),15 createdAt: uint64(sess.createdAt),
16- active: 0,16+ // Snapshot format retains a deterministic first-tab default; it is not
17+ // an interactive attachment selection.
18+ defaultTab: 0,
17 }19 }
18 ephemeral := sess.ephemeral20 ephemeral := sess.ephemeral
19 fallbackCwd := sess.cwd21 fallbackCwd := sess.cwd
M internal/usecase/daemon/snapshot_incremental.go +1 -1
@@ -114,7 +114,7 @@ func (d *Daemon) incrementalPublication(capture *snapshotCapture) (ports.Snapsho
114114 if err := prepareSnapshotChunkCache(capture); err != nil {
115115 return ports.SnapshotPublication{}, err
116116 }
117- manifest := snapcodec.Manifest{Generation: capture.generation, IncarnationID: capture.incarnation, ParentCheckpoint: capture.parentCheckpoint, Name: capture.name, CreatedAt: capture.createdAt, Active: capture.active, Tabs: make([]snapcodec.ManifestTab, 0, len(capture.tabs))}
117+ manifest := snapcodec.Manifest{Generation: capture.generation, IncarnationID: capture.incarnation, ParentCheckpoint: capture.parentCheckpoint, Name: capture.name, CreatedAt: capture.createdAt, Active: capture.defaultTab, Tabs: make([]snapcodec.ManifestTab, 0, len(capture.tabs))}
118118 objects := make([]ports.SnapshotObject, 0)
119119 capture.sealedRefs = make(map[*vt.HistoryChunk]snapcodec.ObjectRef)
120120 for _, tab := range capture.tabs {
@@ -114,7 +114,7 @@ func (d *Daemon) incrementalPublication(capture *snapshotCapture) (ports.Snapsho
114 if err := prepareSnapshotChunkCache(capture); err != nil {114 if err := prepareSnapshotChunkCache(capture); err != nil {
115 return ports.SnapshotPublication{}, err115 return ports.SnapshotPublication{}, err
116 }116 }
117- manifest := snapcodec.Manifest{Generation: capture.generation, IncarnationID: capture.incarnation, ParentCheckpoint: capture.parentCheckpoint, Name: capture.name, CreatedAt: capture.createdAt, Active: capture.active, Tabs: make([]snapcodec.ManifestTab, 0, len(capture.tabs))}117+ manifest := snapcodec.Manifest{Generation: capture.generation, IncarnationID: capture.incarnation, ParentCheckpoint: capture.parentCheckpoint, Name: capture.name, CreatedAt: capture.createdAt, Active: capture.defaultTab, Tabs: make([]snapcodec.ManifestTab, 0, len(capture.tabs))}
118 objects := make([]ports.SnapshotObject, 0)118 objects := make([]ports.SnapshotObject, 0)
119 capture.sealedRefs = make(map[*vt.HistoryChunk]snapcodec.ObjectRef)119 capture.sealedRefs = make(map[*vt.HistoryChunk]snapcodec.ObjectRef)
120 for _, tab := range capture.tabs {120 for _, tab := range capture.tabs {
M internal/usecase/daemon/snapshot_restore.go +2 -0
@@ -290,6 +290,8 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable
290290 }
291291
292292 func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab, term terminalEnv) *session {
293+ // Restored sessions keep ordered tabs; attachment repair deterministically
294+ // selects the first tab instead of restoring an interactive client view.
293295 sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, terminal: term, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)}
294296 // Restored tabs remain private until persistAndRegisterRestoredSession.
295297 // Initialize owners now so registration and reader startup cannot expose an
@@ -290,6 +290,8 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable
290 }290 }
291 291
292 func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab, term terminalEnv) *session {292 func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab, term terminalEnv) *session {
293+ // Restored sessions keep ordered tabs; attachment repair deterministically
294+ // selects the first tab instead of restoring an interactive client view.
293 sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, terminal: term, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)}295 sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, terminal: term, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)}
294 // Restored tabs remain private until persistAndRegisterRestoredSession.296 // Restored tabs remain private until persistAndRegisterRestoredSession.
295 // Initialize owners now so registration and reader startup cannot expose an297 // Initialize owners now so registration and reader startup cannot expose an
M internal/usecase/daemon/snapshot_restore_test.go +1 -1
@@ -233,7 +233,7 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) {
233233 require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean")
234234 require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head")
235235 require.True(t, restored.terminal.TrueColor, "restored sessions must retain startup terminal capability for future panes")
236- require.Equal(t, 0, restored.active)
236+ require.Equal(t, 0, testAttachmentTabIndex(restored))
237237 require.Equal(t, "/snapshot/cwd", restored.cwd)
238238 require.Len(t, restored.tabs, 1)
239239 tab := restored.tabs[0]
@@ -233,7 +233,7 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) {
233 require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean")233 require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean")
234 require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head")234 require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head")
235 require.True(t, restored.terminal.TrueColor, "restored sessions must retain startup terminal capability for future panes")235 require.True(t, restored.terminal.TrueColor, "restored sessions must retain startup terminal capability for future panes")
236- require.Equal(t, 0, restored.active)236+ require.Equal(t, 0, testAttachmentTabIndex(restored))
237 require.Equal(t, "/snapshot/cwd", restored.cwd)237 require.Equal(t, "/snapshot/cwd", restored.cwd)
238 require.Len(t, restored.tabs, 1)238 require.Len(t, restored.tabs, 1)
239 tab := restored.tabs[0]239 tab := restored.tabs[0]
M internal/usecase/daemon/snapshot_resume_publication_test.go +4 -4
@@ -159,7 +159,7 @@ func TestNamedSessionPublishesNextCheckpointAfterClientSnatch(t *testing.T) {
159159 sess, firstClient, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), firstTransport)
160160 require.NoError(t, err)
161161
162- sess.activeTab().focusedPane().screen.Write([]byte("first checkpoint"))
162+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
163163 markSnapshotDirty(sess)
164164 require.True(t, d.scheduleSnapshot(sess))
165165 awaitSnapshotIdle(t, sess)
@@ -181,7 +181,7 @@ func TestNamedSessionPublishesNextCheckpointAfterClientSnatch(t *testing.T) {
181181 require.Same(t, sess, snatchedSession, "client snatch must retain the live session")
182182 require.NotSame(t, firstClient, secondClient, "client B must displace client A")
183183
184- sess.activeTab().focusedPane().screen.Write([]byte("second checkpoint"))
184+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("second checkpoint"))
185185 markSnapshotDirty(sess)
186186 require.True(t, d.scheduleFinalSnapshot(sess))
187187 awaitSnapshotIdle(t, sess)
@@ -223,7 +223,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
223223 sess, _, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), &closeTrackingTransport{})
224224 require.NoError(t, err)
225225
226- sess.activeTab().focusedPane().screen.Write([]byte("first checkpoint"))
226+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
227227 markSnapshotDirty(sess)
228228 require.True(t, d.scheduleSnapshot(sess))
229229 awaitSnapshotClean(t, sess)
@@ -247,7 +247,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
247247 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")
248248 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")
249249
250- resumed.activeTab().focusedPane().screen.Write([]byte("checkpoint after resume"))
250+ testAttachmentTab(resumed).focusedPane().screen.Write([]byte("checkpoint after resume"))
251251 markSnapshotDirty(resumed)
252252 require.True(t, d.scheduleSnapshot(resumed))
253253 awaitSnapshotIdle(t, resumed)
@@ -159,7 +159,7 @@ func TestNamedSessionPublishesNextCheckpointAfterClientSnatch(t *testing.T) {
159 sess, firstClient, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), firstTransport)159 sess, firstClient, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), firstTransport)
160 require.NoError(t, err)160 require.NoError(t, err)
161 161
162- sess.activeTab().focusedPane().screen.Write([]byte("first checkpoint"))162+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
163 markSnapshotDirty(sess)163 markSnapshotDirty(sess)
164 require.True(t, d.scheduleSnapshot(sess))164 require.True(t, d.scheduleSnapshot(sess))
165 awaitSnapshotIdle(t, sess)165 awaitSnapshotIdle(t, sess)
@@ -181,7 +181,7 @@ func TestNamedSessionPublishesNextCheckpointAfterClientSnatch(t *testing.T) {
181 require.Same(t, sess, snatchedSession, "client snatch must retain the live session")181 require.Same(t, sess, snatchedSession, "client snatch must retain the live session")
182 require.NotSame(t, firstClient, secondClient, "client B must displace client A")182 require.NotSame(t, firstClient, secondClient, "client B must displace client A")
183 183
184- sess.activeTab().focusedPane().screen.Write([]byte("second checkpoint"))184+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("second checkpoint"))
185 markSnapshotDirty(sess)185 markSnapshotDirty(sess)
186 require.True(t, d.scheduleFinalSnapshot(sess))186 require.True(t, d.scheduleFinalSnapshot(sess))
187 awaitSnapshotIdle(t, sess)187 awaitSnapshotIdle(t, sess)
@@ -223,7 +223,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
223 sess, _, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), &closeTrackingTransport{})223 sess, _, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), &closeTrackingTransport{})
224 require.NoError(t, err)224 require.NoError(t, err)
225 225
226- sess.activeTab().focusedPane().screen.Write([]byte("first checkpoint"))226+ testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
227 markSnapshotDirty(sess)227 markSnapshotDirty(sess)
228 require.True(t, d.scheduleSnapshot(sess))228 require.True(t, d.scheduleSnapshot(sess))
229 awaitSnapshotClean(t, sess)229 awaitSnapshotClean(t, sess)
@@ -247,7 +247,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
247 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")247 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")
248 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")248 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")
249 249
250- resumed.activeTab().focusedPane().screen.Write([]byte("checkpoint after resume"))250+ testAttachmentTab(resumed).focusedPane().screen.Write([]byte("checkpoint after resume"))
251 markSnapshotDirty(resumed)251 markSnapshotDirty(resumed)
252 require.True(t, d.scheduleSnapshot(resumed))252 require.True(t, d.scheduleSnapshot(resumed))
253 awaitSnapshotIdle(t, resumed)253 awaitSnapshotIdle(t, resumed)
M internal/usecase/daemon/snapshot_test.go +1 -1
@@ -89,7 +89,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin
8989 p := tb.panes["pane-1"]
9090 p.screen.Write([]byte("hello"))
9191 appendHistoryRow(t, p.history, []renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})
92- sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, ephemeral: ephemeral, createdAt: 42}, ctx: context.Background(), cancel: func() {}, tabs: []*tab{tb}, active: 0, cwd: cwd}
92+ sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, ephemeral: ephemeral, createdAt: 42}, ctx: context.Background(), cancel: func() {}, tabs: []*tab{tb}, cwd: cwd}
9393 if !ephemeral {
9494 sess.incarnation = domain.IncarnationID{1}
9595 }
@@ -89,7 +89,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin
89 p := tb.panes["pane-1"]89 p := tb.panes["pane-1"]
90 p.screen.Write([]byte("hello"))90 p.screen.Write([]byte("hello"))
91 appendHistoryRow(t, p.history, []renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})91 appendHistoryRow(t, p.history, []renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})
92- sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, ephemeral: ephemeral, createdAt: 42}, ctx: context.Background(), cancel: func() {}, tabs: []*tab{tb}, active: 0, cwd: cwd}92+ sess := &session{sessionCore: sessionCore{id: domain.SessionID("sess-" + name), name: name, ephemeral: ephemeral, createdAt: 42}, ctx: context.Background(), cancel: func() {}, tabs: []*tab{tb}, cwd: cwd}
93 if !ephemeral {93 if !ephemeral {
94 sess.incarnation = domain.IncarnationID{1}94 sess.incarnation = domain.IncarnationID{1}
95 }95 }
M internal/usecase/daemon/status_test.go +17 -17
@@ -34,7 +34,7 @@ func (*coreNilAttachment) statusSegments(bool) statusSnapshot { return statusSna
3434 func (*coreNilAttachment) capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool) {
3535 return nil, false
3636 }
37-func (*coreNilAttachment) activateTargetLocked(int) bool { return false }
37+func (*coreNilAttachment) validTargetTabLocked(int) bool { return false }
3838 func (*coreNilAttachment) isProxy() bool { return false }
3939
4040 // stubClock returns timers whose channel never fires, so a scheduler under it
@@ -46,7 +46,7 @@ func TestApplyConfigThemeRepaintInvalidatesComposedFrameCache(t *testing.T) {
4646 defer releasePTY()
4747 d.ApplyConfig(domain.Config{Theme: domain.ThemeLight})
4848
49- win := sess.activeTab()
49+ win := testAttachmentTab(sess)
5050 left := win.focusedPane()
5151 right := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 4})
5252 win.mu.Lock()
@@ -89,13 +89,13 @@ func TestApplyConfigThemeRepaintInvalidatesComposedFrameCache(t *testing.T) {
8989 func TestStatusCompositionGolden(t *testing.T) {
9090 p1, releasePTY1 := newBlockingPTY(t)
9191 p2, releasePTY2 := newBlockingPTY(t)
92- _, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
92+ _, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
9393 defer releasePTY1()
9494 defer releasePTY2()
95- sess.active = 1
95+ selectTestAttachmentTab(sess, 1)
9696 sess.name = "work"
9797
98- win := sess.activeTab()
98+ win := testAttachmentTab(sess)
9999 win.focusedPane().screen.Resize(12, 2)
100100 win.size = domain.Size{Cols: 12, Rows: 2}
101101 win.focusedPane().screen.Write([]byte("hello"))
@@ -106,7 +106,7 @@ func TestStatusCompositionGolden(t *testing.T) {
106106 panes: []capturedPaneRenderState{{
107107 id: win.focusedPane().id, frame: win.focusedPane().screen.Frame.Clone(), placement: layout.Placement{ID: win.focusedPane().id, Content: domain.Rect{Width: 12, Height: 2}}, focused: true, damage: []renderer.Damage{renderer.FullRedraw()},
108108 }},
109- bars: barState{status: sess.statusSegments(true)},
109+ bars: barState{status: sess.statusSegmentsFor(ac, true)},
110110 }
111111 composed := composeFrame(frameState, composeCacheInput{})
112112 frame, damage := composed.frame, composed.damage
@@ -418,7 +418,7 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) {
418418 p, release := newBlockingPTY(t)
419419 _, sess, ac, _ := newManualSessionWithPTYs(t, p)
420420 defer release()
421- win := sess.activeTab()
421+ win := testAttachmentTab(sess)
422422 win.focusedPane().screen.Resize(12, 2)
423423 win.size = domain.Size{Cols: 12, Rows: 2}
424424 ac.setThemeForTest(themeui.Theme{
@@ -462,7 +462,7 @@ func TestApplyThemePropagatesToFloatingPane(t *testing.T) {
462462 p, releasePTY := newBlockingPTY(t)
463463 defer releasePTY()
464464 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
465- tb := sess.activeTab()
465+ tb := testAttachmentTab(sess)
466466 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
467467 installTestFloating(tb, floating, true)
468468 clientTheme := ports.Theme{
@@ -694,7 +694,7 @@ func TestAttachClientClearsStaleScreenDefaultColors(t *testing.T) {
694694 require.NoError(t, err)
695695
696696 var got []byte
697- tb := sess.activeTab()
697+ tb := testAttachmentTab(sess)
698698 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
699699 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
700700 require.Empty(t, got)
@@ -710,7 +710,7 @@ func TestClientGoneResetsScreenDefaultColors(t *testing.T) {
710710 })
711711
712712 // Sanity: the tab answers OSC 10/11 while ac is attached.
713- tb := sess.activeTab()
713+ tb := testAttachmentTab(sess)
714714 var before []byte
715715 tb.focusedPane().screen.OnResponse = func(b []byte) { before = append(before, b...) }
716716 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -751,7 +751,7 @@ func TestClientGoneResetDoesNotClobberNewlyAttachedClient(t *testing.T) {
751751 // new client's freshly applied colors.
752752 d.resetScreenDefaultColors(sess)
753753
754- tb := sess.activeTab()
754+ tb := testAttachmentTab(sess)
755755 var got []byte
756756 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
757757 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -769,7 +769,7 @@ func TestDetachOnSendErrorResetsScreenDefaultColors(t *testing.T) {
769769
770770 d.detachOnSendError(sess, ac, ac.transport())
771771
772- tb := sess.activeTab()
772+ tb := testAttachmentTab(sess)
773773 var got []byte
774774 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
775775 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -788,7 +788,7 @@ func TestDetachOnSendErrorParkPreservesScreenDefaultColors(t *testing.T) {
788788
789789 d.detachOnSendError(sess, ac, ac.transport())
790790
791- tb := sess.activeTab()
791+ tb := testAttachmentTab(sess)
792792 var got []byte
793793 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
794794 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -809,7 +809,7 @@ func TestApplyThemeIgnoresReplacedClient(t *testing.T) {
809809 })
810810
811811 var got []byte
812- tb := sess.activeTab()
812+ tb := testAttachmentTab(sess)
813813 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
814814 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
815815 require.Empty(t, got)
@@ -821,7 +821,7 @@ func TestStatusMarksEphemeralSession(t *testing.T) {
821821 defer release()
822822 sess.name = "0"
823823 sess.ephemeral = true
824- win := sess.activeTab()
824+ win := testAttachmentTab(sess)
825825 win.focusedPane().screen.Resize(12, 2)
826826 win.size = domain.Size{Cols: 12, Rows: 2}
827827
@@ -888,7 +888,7 @@ func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) {
888888 _, sess, _, _ := newManualSessionWithPTYs(t, p)
889889 defer releasePTY()
890890 sess.name = "work"
891- win := sess.activeTab()
891+ win := testAttachmentTab(sess)
892892 win.focusedPane().screen.Resize(30, 2)
893893 win.size = domain.Size{Cols: 30, Rows: 2}
894894
@@ -960,7 +960,7 @@ func TestTopBarRendersAttentionBell(t *testing.T) {
960960 _, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
961961 defer releasePTY1()
962962 defer releasePTY2()
963- win := sess.activeTab()
963+ win := testAttachmentTab(sess)
964964 win.focusedPane().screen.Resize(18, 2)
965965 win.size = domain.Size{Cols: 18, Rows: 2}
966966 sess.mu.Lock()
@@ -34,7 +34,7 @@ func (*coreNilAttachment) statusSegments(bool) statusSnapshot { return statusSna
34 func (*coreNilAttachment) capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool) {34 func (*coreNilAttachment) capturePrimary(*attachedClient, primaryCaptureRequest) (*capturedRenderState, bool) {
35 return nil, false35 return nil, false
36 }36 }
37-func (*coreNilAttachment) activateTargetLocked(int) bool { return false }37+func (*coreNilAttachment) validTargetTabLocked(int) bool { return false }
38 func (*coreNilAttachment) isProxy() bool { return false }38 func (*coreNilAttachment) isProxy() bool { return false }
39 39
40 // stubClock returns timers whose channel never fires, so a scheduler under it40 // stubClock returns timers whose channel never fires, so a scheduler under it
@@ -46,7 +46,7 @@ func TestApplyConfigThemeRepaintInvalidatesComposedFrameCache(t *testing.T) {
46 defer releasePTY()46 defer releasePTY()
47 d.ApplyConfig(domain.Config{Theme: domain.ThemeLight})47 d.ApplyConfig(domain.Config{Theme: domain.ThemeLight})
48 48
49- win := sess.activeTab()49+ win := testAttachmentTab(sess)
50 left := win.focusedPane()50 left := win.focusedPane()
51 right := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 4})51 right := newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 4})
52 win.mu.Lock()52 win.mu.Lock()
@@ -89,13 +89,13 @@ func TestApplyConfigThemeRepaintInvalidatesComposedFrameCache(t *testing.T) {
89 func TestStatusCompositionGolden(t *testing.T) {89 func TestStatusCompositionGolden(t *testing.T) {
90 p1, releasePTY1 := newBlockingPTY(t)90 p1, releasePTY1 := newBlockingPTY(t)
91 p2, releasePTY2 := newBlockingPTY(t)91 p2, releasePTY2 := newBlockingPTY(t)
92- _, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)92+ _, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
93 defer releasePTY1()93 defer releasePTY1()
94 defer releasePTY2()94 defer releasePTY2()
95- sess.active = 195+ selectTestAttachmentTab(sess, 1)
96 sess.name = "work"96 sess.name = "work"
97 97
98- win := sess.activeTab()98+ win := testAttachmentTab(sess)
99 win.focusedPane().screen.Resize(12, 2)99 win.focusedPane().screen.Resize(12, 2)
100 win.size = domain.Size{Cols: 12, Rows: 2}100 win.size = domain.Size{Cols: 12, Rows: 2}
101 win.focusedPane().screen.Write([]byte("hello"))101 win.focusedPane().screen.Write([]byte("hello"))
@@ -106,7 +106,7 @@ func TestStatusCompositionGolden(t *testing.T) {
106 panes: []capturedPaneRenderState{{106 panes: []capturedPaneRenderState{{
107 id: win.focusedPane().id, frame: win.focusedPane().screen.Frame.Clone(), placement: layout.Placement{ID: win.focusedPane().id, Content: domain.Rect{Width: 12, Height: 2}}, focused: true, damage: []renderer.Damage{renderer.FullRedraw()},107 id: win.focusedPane().id, frame: win.focusedPane().screen.Frame.Clone(), placement: layout.Placement{ID: win.focusedPane().id, Content: domain.Rect{Width: 12, Height: 2}}, focused: true, damage: []renderer.Damage{renderer.FullRedraw()},
108 }},108 }},
109- bars: barState{status: sess.statusSegments(true)},109+ bars: barState{status: sess.statusSegmentsFor(ac, true)},
110 }110 }
111 composed := composeFrame(frameState, composeCacheInput{})111 composed := composeFrame(frameState, composeCacheInput{})
112 frame, damage := composed.frame, composed.damage112 frame, damage := composed.frame, composed.damage
@@ -418,7 +418,7 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) {
418 p, release := newBlockingPTY(t)418 p, release := newBlockingPTY(t)
419 _, sess, ac, _ := newManualSessionWithPTYs(t, p)419 _, sess, ac, _ := newManualSessionWithPTYs(t, p)
420 defer release()420 defer release()
421- win := sess.activeTab()421+ win := testAttachmentTab(sess)
422 win.focusedPane().screen.Resize(12, 2)422 win.focusedPane().screen.Resize(12, 2)
423 win.size = domain.Size{Cols: 12, Rows: 2}423 win.size = domain.Size{Cols: 12, Rows: 2}
424 ac.setThemeForTest(themeui.Theme{424 ac.setThemeForTest(themeui.Theme{
@@ -462,7 +462,7 @@ func TestApplyThemePropagatesToFloatingPane(t *testing.T) {
462 p, releasePTY := newBlockingPTY(t)462 p, releasePTY := newBlockingPTY(t)
463 defer releasePTY()463 defer releasePTY()
464 d, sess, ac, _ := newManualSessionWithPTYs(t, p)464 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
465- tb := sess.activeTab()465+ tb := testAttachmentTab(sess)
466 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})466 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
467 installTestFloating(tb, floating, true)467 installTestFloating(tb, floating, true)
468 clientTheme := ports.Theme{468 clientTheme := ports.Theme{
@@ -694,7 +694,7 @@ func TestAttachClientClearsStaleScreenDefaultColors(t *testing.T) {
694 require.NoError(t, err)694 require.NoError(t, err)
695 695
696 var got []byte696 var got []byte
697- tb := sess.activeTab()697+ tb := testAttachmentTab(sess)
698 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }698 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
699 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))699 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
700 require.Empty(t, got)700 require.Empty(t, got)
@@ -710,7 +710,7 @@ func TestClientGoneResetsScreenDefaultColors(t *testing.T) {
710 })710 })
711 711
712 // Sanity: the tab answers OSC 10/11 while ac is attached.712 // Sanity: the tab answers OSC 10/11 while ac is attached.
713- tb := sess.activeTab()713+ tb := testAttachmentTab(sess)
714 var before []byte714 var before []byte
715 tb.focusedPane().screen.OnResponse = func(b []byte) { before = append(before, b...) }715 tb.focusedPane().screen.OnResponse = func(b []byte) { before = append(before, b...) }
716 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))716 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -751,7 +751,7 @@ func TestClientGoneResetDoesNotClobberNewlyAttachedClient(t *testing.T) {
751 // new client's freshly applied colors.751 // new client's freshly applied colors.
752 d.resetScreenDefaultColors(sess)752 d.resetScreenDefaultColors(sess)
753 753
754- tb := sess.activeTab()754+ tb := testAttachmentTab(sess)
755 var got []byte755 var got []byte
756 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }756 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
757 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))757 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -769,7 +769,7 @@ func TestDetachOnSendErrorResetsScreenDefaultColors(t *testing.T) {
769 769
770 d.detachOnSendError(sess, ac, ac.transport())770 d.detachOnSendError(sess, ac, ac.transport())
771 771
772- tb := sess.activeTab()772+ tb := testAttachmentTab(sess)
773 var got []byte773 var got []byte
774 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }774 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
775 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))775 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -788,7 +788,7 @@ func TestDetachOnSendErrorParkPreservesScreenDefaultColors(t *testing.T) {
788 788
789 d.detachOnSendError(sess, ac, ac.transport())789 d.detachOnSendError(sess, ac, ac.transport())
790 790
791- tb := sess.activeTab()791+ tb := testAttachmentTab(sess)
792 var got []byte792 var got []byte
793 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }793 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
794 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))794 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
@@ -809,7 +809,7 @@ func TestApplyThemeIgnoresReplacedClient(t *testing.T) {
809 })809 })
810 810
811 var got []byte811 var got []byte
812- tb := sess.activeTab()812+ tb := testAttachmentTab(sess)
813 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }813 tb.focusedPane().screen.OnResponse = func(b []byte) { got = append(got, b...) }
814 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))814 tb.focusedPane().screen.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
815 require.Empty(t, got)815 require.Empty(t, got)
@@ -821,7 +821,7 @@ func TestStatusMarksEphemeralSession(t *testing.T) {
821 defer release()821 defer release()
822 sess.name = "0"822 sess.name = "0"
823 sess.ephemeral = true823 sess.ephemeral = true
824- win := sess.activeTab()824+ win := testAttachmentTab(sess)
825 win.focusedPane().screen.Resize(12, 2)825 win.focusedPane().screen.Resize(12, 2)
826 win.size = domain.Size{Cols: 12, Rows: 2}826 win.size = domain.Size{Cols: 12, Rows: 2}
827 827
@@ -888,7 +888,7 @@ func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) {
888 _, sess, _, _ := newManualSessionWithPTYs(t, p)888 _, sess, _, _ := newManualSessionWithPTYs(t, p)
889 defer releasePTY()889 defer releasePTY()
890 sess.name = "work"890 sess.name = "work"
891- win := sess.activeTab()891+ win := testAttachmentTab(sess)
892 win.focusedPane().screen.Resize(30, 2)892 win.focusedPane().screen.Resize(30, 2)
893 win.size = domain.Size{Cols: 30, Rows: 2}893 win.size = domain.Size{Cols: 30, Rows: 2}
894 894
@@ -960,7 +960,7 @@ func TestTopBarRendersAttentionBell(t *testing.T) {
960 _, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)960 _, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
961 defer releasePTY1()961 defer releasePTY1()
962 defer releasePTY2()962 defer releasePTY2()
963- win := sess.activeTab()963+ win := testAttachmentTab(sess)
964 win.focusedPane().screen.Resize(18, 2)964 win.focusedPane().screen.Resize(18, 2)
965 win.size = domain.Size{Cols: 18, Rows: 2}965 win.size = domain.Size{Cols: 18, Rows: 2}
966 sess.mu.Lock()966 sess.mu.Lock()
M internal/usecase/daemon/transactional_resize_test.go +15 -15
@@ -131,7 +131,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
131131 t.Run("success resizes then replays", func(t *testing.T) {
132132 pty := &transactionalResizePTY{}
133133 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
134- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
134+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
135135 p.resizeApplying = true
136136 p.resizePending = []byte("A\x1b[1;81HB")
137137 d.replayResizePending(sess, tb, p, true, domain.Rect{Width: 120, Height: 23})
@@ -142,7 +142,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
142142 t.Run("failure retains old parser width", func(t *testing.T) {
143143 pty := &transactionalResizePTY{}
144144 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
145- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
145+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
146146 p.resizeApplying = true
147147 p.resizePending = []byte("\x1b[1;81HB")
148148 d.replayResizePending(sess, tb, p, false, domain.Rect{Width: 120, Height: 23})
@@ -152,7 +152,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
152152 t.Run("batches retain read order", func(t *testing.T) {
153153 pty := &transactionalResizePTY{}
154154 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
155- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
155+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
156156 p.screen.OnResponse = func([]byte) { p.ptyResponses = append(p.ptyResponses, 'r') }
157157 pty.onWrite = func([]byte) {
158158 p.mu.Lock()
@@ -171,7 +171,7 @@ func TestProcessPTYDataRetainsCallbacksDuringResizeReplay(t *testing.T) {
171171 pty := &transactionalResizePTY{onWrite: func(b []byte) { responses <- b }}
172172 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
173173 publishActiveClipboardCapability(d, sess, ac, ac.transport())
174- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
174+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
175175 p.screen.OnResponse = func(b []byte) { p.ptyResponses = append(p.ptyResponses, b...) }
176176 p.screen.OnBell = func() { p.ptyAttention = true }
177177 p.screen.OnClipboard = func(b64 string) { p.ptyClipboards = append(p.ptyClipboards, b64) }
@@ -207,7 +207,7 @@ func TestProcessPTYDataRetainsCallbacksDuringResizeReplay(t *testing.T) {
207207 func TestApplySessionLayoutStopsRetryWhenSessionCanceled(t *testing.T) {
208208 pty := &transactionalResizePTY{}
209209 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
210- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
210+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
211211 d.beforeSessionResizePublication = sess.cancel
212212
213213 _, ok := d.applySessionLayout(sess, domain.Size{Cols: 100, Rows: 30}, nil, nil)
@@ -226,7 +226,7 @@ func TestApplySessionLayoutStopsRetryWhenSessionCanceled(t *testing.T) {
226226 func TestTransactionalResizeApplyDoesNotHoldTabOrPaneLocks(t *testing.T) {
227227 pty := &transactionalResizePTY{}
228228 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
229- tb := sess.activeTab()
229+ tb := testAttachmentTab(sess)
230230 p := tb.focusedPane()
231231 var tabUnlocked, paneUnlocked bool
232232 pty.onResize = func() {
@@ -291,7 +291,7 @@ func TestTransactionalResizePartialFailureCommitsOnlySuccessfulPTYState(t *testi
291291 ok := &transactionalResizePTY{}
292292 failed := &transactionalResizePTY{errs: []error{errors.New("scripted resize failure")}}
293293 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)
294- tb := sess.activeTab()
294+ tb := testAttachmentTab(sess)
295295 second := newPane("pane-2", failed, domain.Size{Cols: 80, Rows: 23})
296296 tb.mu.Lock()
297297 tb.panes[second.id] = second
@@ -332,7 +332,7 @@ func TestTransactionalResizeRetryMarksNamedSnapshotDirtyOnlyOnSuccess(t *testing
332332 pty := &transactionalResizePTY{errs: tc.errs}
333333 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)
334334 sess.snapEligible.Store(true)
335- tb, p := sess.activeTab(), sess.activeTab().focusedPane()
335+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
336336
337337 require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 100, Rows: 30}, true))
338338 rc := sess.renderCoordinator()
@@ -356,7 +356,7 @@ func TestTransactionalResizeRetryTargetsNewestCommittedEpoch(t *testing.T) {
356356 ok := &transactionalResizePTY{}
357357 failed := &transactionalResizePTY{errs: []error{errors.New("first epoch fails"), nil}}
358358 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)
359- tb := sess.activeTab()
359+ tb := testAttachmentTab(sess)
360360 retry := newPane("retry", failed, domain.Size{Cols: 80, Rows: 23})
361361 tb.mu.Lock()
362362 tb.panes[retry.id] = retry
@@ -465,7 +465,7 @@ func TestTransactionalResizeRejectsNewerEpochBeforeSessionPublication(t *testing
465465 func TestStaleRemovedMemberGateIsCanceledOnceByFreshPlan(t *testing.T) {
466466 pty := &transactionalResizePTY{}
467467 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
468- tb := sess.activeTab()
468+ tb := testAttachmentTab(sess)
469469 p := tb.focusedPane()
470470 p.resizeApplying = true
471471 p.resizePending = []byte("x")
@@ -489,7 +489,7 @@ func TestStaleRemovedMemberGateIsCanceledOnceByFreshPlan(t *testing.T) {
489489 func TestHeadlessResizeRetriesInvalidatedPlan(t *testing.T) {
490490 pty := &transactionalResizePTY{}
491491 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
492- tb := sess.activeTab()
492+ tb := testAttachmentTab(sess)
493493 var invalidate sync.Once
494494 pty.onResize = func() {
495495 invalidate.Do(func() {
@@ -509,7 +509,7 @@ func TestHeadlessResizeRetriesInvalidatedPlan(t *testing.T) {
509509 func TestRetryOwnerCannotPublishFloatingGeometryAfterMove(t *testing.T) {
510510 popupPTY := &transactionalResizePTY{errs: []error{errors.New("initial popup failure")}}
511511 d, source, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
512- tb := source.activeTab()
512+ tb := testAttachmentTab(source)
513513 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})
514514 tb.mu.Lock()
515515 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}
@@ -561,7 +561,7 @@ func TestTransactionalResizeFloatingBreakpointRace(t *testing.T) {
561561 d.ApplyConfig(domain.Config{Floating: cfg})
562562 require.True(t, d.requestTransactionalResize(sess, nil, tc.from, true))
563563
564- tb := sess.activeTab()
564+ tb := testAttachmentTab(sess)
565565 fromGeometry := calculateContentFloatingGeometry(tabSize(tc.from), cfg)
566566 toGeometry := calculateContentFloatingGeometry(tabSize(tc.to), cfg)
567567 entered, release := make(chan struct{}), make(chan struct{})
@@ -735,7 +735,7 @@ func TestZeroInnerFloatingDrawerRejectsStaleRequestWithoutPhysicalPublication(t
735735 func TestTransactionalResizeRetriesAcceptedFloatingSlotByIdentity(t *testing.T) {
736736 popupPTY := &transactionalResizePTY{errs: []error{errors.New("first popup resize fails")}}
737737 d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
738- tb := sess.activeTab()
738+ tb := testAttachmentTab(sess)
739739 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})
740740 tb.mu.Lock()
741741 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}
@@ -823,7 +823,7 @@ func TestTransactionalResizeFloatingMouseCopyAndSearchContract(t *testing.T) {
823823 entered, release := make(chan struct{}), make(chan struct{})
824824 pty := &transactionalResizePTY{errs: tc.errs, onResize: func() { close(entered); <-release }}
825825 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)
826- tb := sess.activeTab()
826+ tb := testAttachmentTab(sess)
827827 p := tb.focusedPane()
828828 old := p.rect
829829 d.enterCopyMode(sess, ac)
@@ -131,7 +131,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
131 t.Run("success resizes then replays", func(t *testing.T) {131 t.Run("success resizes then replays", func(t *testing.T) {
132 pty := &transactionalResizePTY{}132 pty := &transactionalResizePTY{}
133 d, sess, _, _ := newManualSessionWithPTYs(t, pty)133 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
134- tb, p := sess.activeTab(), sess.activeTab().focusedPane()134+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
135 p.resizeApplying = true135 p.resizeApplying = true
136 p.resizePending = []byte("A\x1b[1;81HB")136 p.resizePending = []byte("A\x1b[1;81HB")
137 d.replayResizePending(sess, tb, p, true, domain.Rect{Width: 120, Height: 23})137 d.replayResizePending(sess, tb, p, true, domain.Rect{Width: 120, Height: 23})
@@ -142,7 +142,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
142 t.Run("failure retains old parser width", func(t *testing.T) {142 t.Run("failure retains old parser width", func(t *testing.T) {
143 pty := &transactionalResizePTY{}143 pty := &transactionalResizePTY{}
144 d, sess, _, _ := newManualSessionWithPTYs(t, pty)144 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
145- tb, p := sess.activeTab(), sess.activeTab().focusedPane()145+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
146 p.resizeApplying = true146 p.resizeApplying = true
147 p.resizePending = []byte("\x1b[1;81HB")147 p.resizePending = []byte("\x1b[1;81HB")
148 d.replayResizePending(sess, tb, p, false, domain.Rect{Width: 120, Height: 23})148 d.replayResizePending(sess, tb, p, false, domain.Rect{Width: 120, Height: 23})
@@ -152,7 +152,7 @@ func TestReplayResizePendingBuffersSuccessFailureAndBatchOrder(t *testing.T) {
152 t.Run("batches retain read order", func(t *testing.T) {152 t.Run("batches retain read order", func(t *testing.T) {
153 pty := &transactionalResizePTY{}153 pty := &transactionalResizePTY{}
154 d, sess, _, _ := newManualSessionWithPTYs(t, pty)154 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
155- tb, p := sess.activeTab(), sess.activeTab().focusedPane()155+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
156 p.screen.OnResponse = func([]byte) { p.ptyResponses = append(p.ptyResponses, 'r') }156 p.screen.OnResponse = func([]byte) { p.ptyResponses = append(p.ptyResponses, 'r') }
157 pty.onWrite = func([]byte) {157 pty.onWrite = func([]byte) {
158 p.mu.Lock()158 p.mu.Lock()
@@ -171,7 +171,7 @@ func TestProcessPTYDataRetainsCallbacksDuringResizeReplay(t *testing.T) {
171 pty := &transactionalResizePTY{onWrite: func(b []byte) { responses <- b }}171 pty := &transactionalResizePTY{onWrite: func(b []byte) { responses <- b }}
172 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)172 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
173 publishActiveClipboardCapability(d, sess, ac, ac.transport())173 publishActiveClipboardCapability(d, sess, ac, ac.transport())
174- tb, p := sess.activeTab(), sess.activeTab().focusedPane()174+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
175 p.screen.OnResponse = func(b []byte) { p.ptyResponses = append(p.ptyResponses, b...) }175 p.screen.OnResponse = func(b []byte) { p.ptyResponses = append(p.ptyResponses, b...) }
176 p.screen.OnBell = func() { p.ptyAttention = true }176 p.screen.OnBell = func() { p.ptyAttention = true }
177 p.screen.OnClipboard = func(b64 string) { p.ptyClipboards = append(p.ptyClipboards, b64) }177 p.screen.OnClipboard = func(b64 string) { p.ptyClipboards = append(p.ptyClipboards, b64) }
@@ -207,7 +207,7 @@ func TestProcessPTYDataRetainsCallbacksDuringResizeReplay(t *testing.T) {
207 func TestApplySessionLayoutStopsRetryWhenSessionCanceled(t *testing.T) {207 func TestApplySessionLayoutStopsRetryWhenSessionCanceled(t *testing.T) {
208 pty := &transactionalResizePTY{}208 pty := &transactionalResizePTY{}
209 d, sess, _, _ := newManualSessionWithPTYs(t, pty)209 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
210- tb, p := sess.activeTab(), sess.activeTab().focusedPane()210+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
211 d.beforeSessionResizePublication = sess.cancel211 d.beforeSessionResizePublication = sess.cancel
212 212
213 _, ok := d.applySessionLayout(sess, domain.Size{Cols: 100, Rows: 30}, nil, nil)213 _, ok := d.applySessionLayout(sess, domain.Size{Cols: 100, Rows: 30}, nil, nil)
@@ -226,7 +226,7 @@ func TestApplySessionLayoutStopsRetryWhenSessionCanceled(t *testing.T) {
226 func TestTransactionalResizeApplyDoesNotHoldTabOrPaneLocks(t *testing.T) {226 func TestTransactionalResizeApplyDoesNotHoldTabOrPaneLocks(t *testing.T) {
227 pty := &transactionalResizePTY{}227 pty := &transactionalResizePTY{}
228 d, sess, _, _ := newManualSessionWithPTYs(t, pty)228 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
229- tb := sess.activeTab()229+ tb := testAttachmentTab(sess)
230 p := tb.focusedPane()230 p := tb.focusedPane()
231 var tabUnlocked, paneUnlocked bool231 var tabUnlocked, paneUnlocked bool
232 pty.onResize = func() {232 pty.onResize = func() {
@@ -291,7 +291,7 @@ func TestTransactionalResizePartialFailureCommitsOnlySuccessfulPTYState(t *testi
291 ok := &transactionalResizePTY{}291 ok := &transactionalResizePTY{}
292 failed := &transactionalResizePTY{errs: []error{errors.New("scripted resize failure")}}292 failed := &transactionalResizePTY{errs: []error{errors.New("scripted resize failure")}}
293 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)293 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)
294- tb := sess.activeTab()294+ tb := testAttachmentTab(sess)
295 second := newPane("pane-2", failed, domain.Size{Cols: 80, Rows: 23})295 second := newPane("pane-2", failed, domain.Size{Cols: 80, Rows: 23})
296 tb.mu.Lock()296 tb.mu.Lock()
297 tb.panes[second.id] = second297 tb.panes[second.id] = second
@@ -332,7 +332,7 @@ func TestTransactionalResizeRetryMarksNamedSnapshotDirtyOnlyOnSuccess(t *testing
332 pty := &transactionalResizePTY{errs: tc.errs}332 pty := &transactionalResizePTY{errs: tc.errs}
333 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)333 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)
334 sess.snapEligible.Store(true)334 sess.snapEligible.Store(true)
335- tb, p := sess.activeTab(), sess.activeTab().focusedPane()335+ tb, p := testAttachmentTab(sess), testAttachmentTab(sess).focusedPane()
336 336
337 require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 100, Rows: 30}, true))337 require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 100, Rows: 30}, true))
338 rc := sess.renderCoordinator()338 rc := sess.renderCoordinator()
@@ -356,7 +356,7 @@ func TestTransactionalResizeRetryTargetsNewestCommittedEpoch(t *testing.T) {
356 ok := &transactionalResizePTY{}356 ok := &transactionalResizePTY{}
357 failed := &transactionalResizePTY{errs: []error{errors.New("first epoch fails"), nil}}357 failed := &transactionalResizePTY{errs: []error{errors.New("first epoch fails"), nil}}
358 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)358 d, sess, ac, _ := newManualSessionWithPTYs(t, ok)
359- tb := sess.activeTab()359+ tb := testAttachmentTab(sess)
360 retry := newPane("retry", failed, domain.Size{Cols: 80, Rows: 23})360 retry := newPane("retry", failed, domain.Size{Cols: 80, Rows: 23})
361 tb.mu.Lock()361 tb.mu.Lock()
362 tb.panes[retry.id] = retry362 tb.panes[retry.id] = retry
@@ -465,7 +465,7 @@ func TestTransactionalResizeRejectsNewerEpochBeforeSessionPublication(t *testing
465 func TestStaleRemovedMemberGateIsCanceledOnceByFreshPlan(t *testing.T) {465 func TestStaleRemovedMemberGateIsCanceledOnceByFreshPlan(t *testing.T) {
466 pty := &transactionalResizePTY{}466 pty := &transactionalResizePTY{}
467 d, sess, _, _ := newManualSessionWithPTYs(t, pty)467 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
468- tb := sess.activeTab()468+ tb := testAttachmentTab(sess)
469 p := tb.focusedPane()469 p := tb.focusedPane()
470 p.resizeApplying = true470 p.resizeApplying = true
471 p.resizePending = []byte("x")471 p.resizePending = []byte("x")
@@ -489,7 +489,7 @@ func TestStaleRemovedMemberGateIsCanceledOnceByFreshPlan(t *testing.T) {
489 func TestHeadlessResizeRetriesInvalidatedPlan(t *testing.T) {489 func TestHeadlessResizeRetriesInvalidatedPlan(t *testing.T) {
490 pty := &transactionalResizePTY{}490 pty := &transactionalResizePTY{}
491 d, sess, _, _ := newManualSessionWithPTYs(t, pty)491 d, sess, _, _ := newManualSessionWithPTYs(t, pty)
492- tb := sess.activeTab()492+ tb := testAttachmentTab(sess)
493 var invalidate sync.Once493 var invalidate sync.Once
494 pty.onResize = func() {494 pty.onResize = func() {
495 invalidate.Do(func() {495 invalidate.Do(func() {
@@ -509,7 +509,7 @@ func TestHeadlessResizeRetriesInvalidatedPlan(t *testing.T) {
509 func TestRetryOwnerCannotPublishFloatingGeometryAfterMove(t *testing.T) {509 func TestRetryOwnerCannotPublishFloatingGeometryAfterMove(t *testing.T) {
510 popupPTY := &transactionalResizePTY{errs: []error{errors.New("initial popup failure")}}510 popupPTY := &transactionalResizePTY{errs: []error{errors.New("initial popup failure")}}
511 d, source, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})511 d, source, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
512- tb := source.activeTab()512+ tb := testAttachmentTab(source)
513 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})513 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})
514 tb.mu.Lock()514 tb.mu.Lock()
515 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}515 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}
@@ -561,7 +561,7 @@ func TestTransactionalResizeFloatingBreakpointRace(t *testing.T) {
561 d.ApplyConfig(domain.Config{Floating: cfg})561 d.ApplyConfig(domain.Config{Floating: cfg})
562 require.True(t, d.requestTransactionalResize(sess, nil, tc.from, true))562 require.True(t, d.requestTransactionalResize(sess, nil, tc.from, true))
563 563
564- tb := sess.activeTab()564+ tb := testAttachmentTab(sess)
565 fromGeometry := calculateContentFloatingGeometry(tabSize(tc.from), cfg)565 fromGeometry := calculateContentFloatingGeometry(tabSize(tc.from), cfg)
566 toGeometry := calculateContentFloatingGeometry(tabSize(tc.to), cfg)566 toGeometry := calculateContentFloatingGeometry(tabSize(tc.to), cfg)
567 entered, release := make(chan struct{}), make(chan struct{})567 entered, release := make(chan struct{}), make(chan struct{})
@@ -735,7 +735,7 @@ func TestZeroInnerFloatingDrawerRejectsStaleRequestWithoutPhysicalPublication(t
735 func TestTransactionalResizeRetriesAcceptedFloatingSlotByIdentity(t *testing.T) {735 func TestTransactionalResizeRetriesAcceptedFloatingSlotByIdentity(t *testing.T) {
736 popupPTY := &transactionalResizePTY{errs: []error{errors.New("first popup resize fails")}}736 popupPTY := &transactionalResizePTY{errs: []error{errors.New("first popup resize fails")}}
737 d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})737 d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
738- tb := sess.activeTab()738+ tb := testAttachmentTab(sess)
739 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})739 popup := newPane("popup", popupPTY, domain.Size{Cols: 80, Rows: 23})
740 tb.mu.Lock()740 tb.mu.Lock()
741 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}741 tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7}
@@ -823,7 +823,7 @@ func TestTransactionalResizeFloatingMouseCopyAndSearchContract(t *testing.T) {
823 entered, release := make(chan struct{}), make(chan struct{})823 entered, release := make(chan struct{}), make(chan struct{})
824 pty := &transactionalResizePTY{errs: tc.errs, onResize: func() { close(entered); <-release }}824 pty := &transactionalResizePTY{errs: tc.errs, onResize: func() { close(entered); <-release }}
825 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)825 d, sess, ac, _ := newManualSessionWithPTYs(t, pty)
826- tb := sess.activeTab()826+ tb := testAttachmentTab(sess)
827 p := tb.focusedPane()827 p := tb.focusedPane()
828 old := p.rect828 old := p.rect
829 d.enterCopyMode(sess, ac)829 d.enterCopyMode(sess, ac)
M internal/usecase/daemon/transport_observability_test.go +2 -2
@@ -125,7 +125,7 @@ func TestTransportObservabilityBlockedRenderObserverReleasesArchitectureLocks(t
125125 }
126126 ac.ackOutputState(output.NewStateNum)
127127
128- pane := sess.activeTab().focusedPane()
128+ pane := testAttachmentTab(sess).focusedPane()
129129 for _, lock := range []struct {
130130 name string
131131 fn func()
@@ -141,7 +141,7 @@ func TestTransportObservabilityBlockedRenderObserverReleasesArchitectureLocks(t
141141
142142 livePTY := make(chan struct{})
143143 go func() {
144- d.processPTYData(sess, sess.activeTab(), pane, []byte("live"), false)
144+ d.processPTYData(sess, testAttachmentTab(sess), pane, []byte("live"), false)
145145 close(livePTY)
146146 }()
147147 awaitDaemonObserver(t, livePTY, "live PTY processing")
@@ -125,7 +125,7 @@ func TestTransportObservabilityBlockedRenderObserverReleasesArchitectureLocks(t
125 }125 }
126 ac.ackOutputState(output.NewStateNum)126 ac.ackOutputState(output.NewStateNum)
127 127
128- pane := sess.activeTab().focusedPane()128+ pane := testAttachmentTab(sess).focusedPane()
129 for _, lock := range []struct {129 for _, lock := range []struct {
130 name string130 name string
131 fn func()131 fn func()
@@ -141,7 +141,7 @@ func TestTransportObservabilityBlockedRenderObserverReleasesArchitectureLocks(t
141 141
142 livePTY := make(chan struct{})142 livePTY := make(chan struct{})
143 go func() {143 go func() {
144- d.processPTYData(sess, sess.activeTab(), pane, []byte("live"), false)144+ d.processPTYData(sess, testAttachmentTab(sess), pane, []byte("live"), false)
145 close(livePTY)145 close(livePTY)
146 }()146 }()
147 awaitDaemonObserver(t, livePTY, "live PTY processing")147 awaitDaemonObserver(t, livePTY, "live PTY processing")

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