detached 6fb053b

refactor(daemon): drop the legacy attachment size field and clock fallbacks

attachedClient now keeps its window size only in geometry. noteAttention
reads the daemon clock without a wall-clock fallback, and the test stub clock
reports a fixed non-zero instant. The test-only Daemon.childEnv helper is
removed in favour of childEnvFrom.
commit
6fb053b192f640d001033c260df797a03c5b0802
author
brice · · 2026-10-08 05:03 UTC
parent
4c16dd8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
31 files · +97 -108
M internal/usecase/daemon/attachment_effect_gate_test.go +10 -10
@@ -198,11 +198,11 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi
198198 first.replaceTransport(firstTransport)
199199
200200 blockedTransport := &closeTrackingTransport{}
201- blocked := &attachedClient{tr: blockedTransport, output: newOutputStateStream(), size: first.size}
201+ blocked := &attachedClient{tr: blockedTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: first.geometry.Size}}
202202 blocked.initOverlays()
203203 blocked.setSession(sess)
204204 laterTransport := &closeTrackingTransport{}
205- later := &attachedClient{tr: laterTransport, output: newOutputStateStream(), size: first.size}
205+ later := &attachedClient{tr: laterTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: first.geometry.Size}}
206206 later.initOverlays()
207207 later.setSession(sess)
208208 sess.mu.Lock()
@@ -320,7 +320,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
320320 snatched.installTestAttachmentCapability(sess.captureAttachmentCapability(snatched, snatchedTransport))
321321
322322 activeTransport := newTeardownBlockingTransport()
323- active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), size: snatched.size}
323+ active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: snatched.geometry.Size}}
324324 active.initOverlays()
325325 transition, err := d.transitionAttachment(attachmentTransitionRequest{
326326 target: sess, next: active, expectedTransport: active.transportSnapshot(), ready: true,
@@ -329,7 +329,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
329329 defer d.deferAttachmentTransitionCleanups(transition)
330330
331331 unrelatedTransport := &closeTrackingTransport{}
332- unrelatedClient := &attachedClient{tr: unrelatedTransport, output: newOutputStateStream(), size: active.size}
332+ unrelatedClient := &attachedClient{tr: unrelatedTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: active.geometry.Size}}
333333 unrelatedClient.initOverlays()
334334 unrelated := &session{sessionCore: sessionCore{id: "unrelated", name: "unrelated", attachments: map[*attachedClient]struct{}{unrelatedClient: {}}}, ctx: sess.ctx, cancel: func() {}}
335335 unrelatedClient.setSession(unrelated)
@@ -505,7 +505,7 @@ func TestAttachmentEffectGateReplacementInterruptsBlockedOldRenderBeforePublicat
505505 <-oldTransport.sendEntered
506506
507507 newTransport := &closeTrackingTransport{}
508- next := &attachedClient{tr: newTransport, output: newOutputStateStream(), size: old.size}
508+ next := &attachedClient{tr: newTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
509509 next.initOverlays()
510510 transitionDone := make(chan attachmentTransitionResult, 1)
511511 transitionErr := make(chan error, 1)
@@ -554,7 +554,7 @@ func TestAttachmentEffectGateAdmittedEffectCompletesBeforeConflictingTransition(
554554 ticket, ok := old.beginAttachmentEffect(token)
555555 require.True(t, ok)
556556
557- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}
557+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
558558 next.initOverlays()
559559 done := make(chan error, 1)
560560 go func() {
@@ -668,7 +668,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
668668 }()
669669 <-admitted
670670
671- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}
671+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
672672 next.initOverlays()
673673 transitionDone := make(chan error, 1)
674674 go func() {
@@ -817,7 +817,7 @@ func TestPickerDeleteDoesNotDeleteSourceAfterInitiatorReplacement(t *testing.T)
817817 }()
818818 <-admissionEnded
819819
820- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}
820+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
821821 next.initOverlays()
822822 result, err := d.transitionAttachment(attachmentTransitionRequest{
823823 target: sess, next: next, expectedTransport: next.transportSnapshot(), ready: true,
@@ -873,7 +873,7 @@ func TestAttachmentEffectGateAdmittedFirstPaintFinishesBeforeReplacement(t *test
873873 go func() { paintDone <- d.firstPaintForTransition(token) }()
874874 <-admitted
875875
876- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}
876+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
877877 next.initOverlays()
878878 transitionDone := make(chan error, 1)
879879 go func() {
@@ -897,7 +897,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi
897897 first.captureAttachmentCapability(a, aTransport)
898898
899899 bTransport := newDatagramTestTransport()
900- b := &attachedClient{tr: bTransport, output: newOutputStateStream(), size: a.size}
900+ b := &attachedClient{tr: bTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: a.geometry.Size}}
901901 b.initOverlays()
902902 second := &session{sessionCore: sessionCore{id: "second", name: "second", attachments: map[*attachedClient]struct{}{b: {}}}}
903903 b.setSession(second)
@@ -198,11 +198,11 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi
198 first.replaceTransport(firstTransport)198 first.replaceTransport(firstTransport)
199 199
200 blockedTransport := &closeTrackingTransport{}200 blockedTransport := &closeTrackingTransport{}
201- blocked := &attachedClient{tr: blockedTransport, output: newOutputStateStream(), size: first.size}201+ blocked := &attachedClient{tr: blockedTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: first.geometry.Size}}
202 blocked.initOverlays()202 blocked.initOverlays()
203 blocked.setSession(sess)203 blocked.setSession(sess)
204 laterTransport := &closeTrackingTransport{}204 laterTransport := &closeTrackingTransport{}
205- later := &attachedClient{tr: laterTransport, output: newOutputStateStream(), size: first.size}205+ later := &attachedClient{tr: laterTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: first.geometry.Size}}
206 later.initOverlays()206 later.initOverlays()
207 later.setSession(sess)207 later.setSession(sess)
208 sess.mu.Lock()208 sess.mu.Lock()
@@ -320,7 +320,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
320 snatched.installTestAttachmentCapability(sess.captureAttachmentCapability(snatched, snatchedTransport))320 snatched.installTestAttachmentCapability(sess.captureAttachmentCapability(snatched, snatchedTransport))
321 321
322 activeTransport := newTeardownBlockingTransport()322 activeTransport := newTeardownBlockingTransport()
323- active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), size: snatched.size}323+ active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: snatched.geometry.Size}}
324 active.initOverlays()324 active.initOverlays()
325 transition, err := d.transitionAttachment(attachmentTransitionRequest{325 transition, err := d.transitionAttachment(attachmentTransitionRequest{
326 target: sess, next: active, expectedTransport: active.transportSnapshot(), ready: true,326 target: sess, next: active, expectedTransport: active.transportSnapshot(), ready: true,
@@ -329,7 +329,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
329 defer d.deferAttachmentTransitionCleanups(transition)329 defer d.deferAttachmentTransitionCleanups(transition)
330 330
331 unrelatedTransport := &closeTrackingTransport{}331 unrelatedTransport := &closeTrackingTransport{}
332- unrelatedClient := &attachedClient{tr: unrelatedTransport, output: newOutputStateStream(), size: active.size}332+ unrelatedClient := &attachedClient{tr: unrelatedTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: active.geometry.Size}}
333 unrelatedClient.initOverlays()333 unrelatedClient.initOverlays()
334 unrelated := &session{sessionCore: sessionCore{id: "unrelated", name: "unrelated", attachments: map[*attachedClient]struct{}{unrelatedClient: {}}}, ctx: sess.ctx, cancel: func() {}}334 unrelated := &session{sessionCore: sessionCore{id: "unrelated", name: "unrelated", attachments: map[*attachedClient]struct{}{unrelatedClient: {}}}, ctx: sess.ctx, cancel: func() {}}
335 unrelatedClient.setSession(unrelated)335 unrelatedClient.setSession(unrelated)
@@ -505,7 +505,7 @@ func TestAttachmentEffectGateReplacementInterruptsBlockedOldRenderBeforePublicat
505 <-oldTransport.sendEntered505 <-oldTransport.sendEntered
506 506
507 newTransport := &closeTrackingTransport{}507 newTransport := &closeTrackingTransport{}
508- next := &attachedClient{tr: newTransport, output: newOutputStateStream(), size: old.size}508+ next := &attachedClient{tr: newTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
509 next.initOverlays()509 next.initOverlays()
510 transitionDone := make(chan attachmentTransitionResult, 1)510 transitionDone := make(chan attachmentTransitionResult, 1)
511 transitionErr := make(chan error, 1)511 transitionErr := make(chan error, 1)
@@ -554,7 +554,7 @@ func TestAttachmentEffectGateAdmittedEffectCompletesBeforeConflictingTransition(
554 ticket, ok := old.beginAttachmentEffect(token)554 ticket, ok := old.beginAttachmentEffect(token)
555 require.True(t, ok)555 require.True(t, ok)
556 556
557- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}557+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
558 next.initOverlays()558 next.initOverlays()
559 done := make(chan error, 1)559 done := make(chan error, 1)
560 go func() {560 go func() {
@@ -668,7 +668,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
668 }()668 }()
669 <-admitted669 <-admitted
670 670
671- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}671+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
672 next.initOverlays()672 next.initOverlays()
673 transitionDone := make(chan error, 1)673 transitionDone := make(chan error, 1)
674 go func() {674 go func() {
@@ -817,7 +817,7 @@ func TestPickerDeleteDoesNotDeleteSourceAfterInitiatorReplacement(t *testing.T)
817 }()817 }()
818 <-admissionEnded818 <-admissionEnded
819 819
820- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}820+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
821 next.initOverlays()821 next.initOverlays()
822 result, err := d.transitionAttachment(attachmentTransitionRequest{822 result, err := d.transitionAttachment(attachmentTransitionRequest{
823 target: sess, next: next, expectedTransport: next.transportSnapshot(), ready: true,823 target: sess, next: next, expectedTransport: next.transportSnapshot(), ready: true,
@@ -873,7 +873,7 @@ func TestAttachmentEffectGateAdmittedFirstPaintFinishesBeforeReplacement(t *test
873 go func() { paintDone <- d.firstPaintForTransition(token) }()873 go func() { paintDone <- d.firstPaintForTransition(token) }()
874 <-admitted874 <-admitted
875 875
876- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}876+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
877 next.initOverlays()877 next.initOverlays()
878 transitionDone := make(chan error, 1)878 transitionDone := make(chan error, 1)
879 go func() {879 go func() {
@@ -897,7 +897,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi
897 first.captureAttachmentCapability(a, aTransport)897 first.captureAttachmentCapability(a, aTransport)
898 898
899 bTransport := newDatagramTestTransport()899 bTransport := newDatagramTestTransport()
900- b := &attachedClient{tr: bTransport, output: newOutputStateStream(), size: a.size}900+ b := &attachedClient{tr: bTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: a.geometry.Size}}
901 b.initOverlays()901 b.initOverlays()
902 second := &session{sessionCore: sessionCore{id: "second", name: "second", attachments: map[*attachedClient]struct{}{b: {}}}}902 second := &session{sessionCore: sessionCore{id: "second", name: "second", attachments: map[*attachedClient]struct{}{b: {}}}}
903 b.setSession(second)903 b.setSession(second)
M internal/usecase/daemon/attachment_output_test.go +2 -2
@@ -122,7 +122,7 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
122122 fillOutputStateRows(frame, []string{"abc"})
123123 newAttachment := func() *attachmentOutput {
124124 stream := newOutputStateStream()
125- ac := &attachedClient{output: stream, size: domain.Size{Cols: 3, Rows: 1}}
125+ ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
126126 stream.attachment = ac
127127 return stream
128128 }
@@ -195,7 +195,7 @@ func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) {
195195 } {
196196 t.Run(tt.name, func(t *testing.T) {
197197 stream := newOutputStateStream()
198- ac := &attachedClient{output: stream, size: domain.Size{Cols: 3, Rows: 1}}
198+ ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
199199 stream.attachment = ac
200200 frame := renderer.NewFrame(3, 1)
201201 fillOutputStateRows(frame, []string{"abc"})
@@ -122,7 +122,7 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
122 fillOutputStateRows(frame, []string{"abc"})122 fillOutputStateRows(frame, []string{"abc"})
123 newAttachment := func() *attachmentOutput {123 newAttachment := func() *attachmentOutput {
124 stream := newOutputStateStream()124 stream := newOutputStateStream()
125- ac := &attachedClient{output: stream, size: domain.Size{Cols: 3, Rows: 1}}125+ ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
126 stream.attachment = ac126 stream.attachment = ac
127 return stream127 return stream
128 }128 }
@@ -195,7 +195,7 @@ func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) {
195 } {195 } {
196 t.Run(tt.name, func(t *testing.T) {196 t.Run(tt.name, func(t *testing.T) {
197 stream := newOutputStateStream()197 stream := newOutputStateStream()
198- ac := &attachedClient{output: stream, size: domain.Size{Cols: 3, Rows: 1}}198+ ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
199 stream.attachment = ac199 stream.attachment = ac
200 frame := renderer.NewFrame(3, 1)200 frame := renderer.NewFrame(3, 1)
201 fillOutputStateRows(frame, []string{"abc"})201 fillOutputStateRows(frame, []string{"abc"})
M internal/usecase/daemon/attachment_registry_test.go +2 -2
@@ -77,7 +77,7 @@ func TestAttachmentRegistryRepairsRemovedStableTarget(t *testing.T) {
7777 func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.T) {
7878 sess := registryTestSession()
7979 ac := registryTestAttachment(1)
80- ac.size = domain.Size{Cols: 80, Rows: 23}
80+ ac.geometry.Size = domain.Size{Cols: 80, Rows: 23}
8181 ac.output = newOutputStateStream()
8282 ac.output.attachment = ac
8383 require.True(t, sess.registerAttachment(ac))
@@ -105,7 +105,7 @@ func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.
105105 func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {
106106 sess := registryTestSession()
107107 ac := registryTestAttachment(1)
108- ac.size = domain.Size{Cols: 80, Rows: 23}
108+ ac.geometry.Size = domain.Size{Cols: 80, Rows: 23}
109109 ac.output = newOutputStateStream()
110110 ac.output.attachment = ac
111111 require.True(t, sess.registerAttachment(ac))
@@ -77,7 +77,7 @@ func TestAttachmentRegistryRepairsRemovedStableTarget(t *testing.T) {
77 func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.T) {77 func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.T) {
78 sess := registryTestSession()78 sess := registryTestSession()
79 ac := registryTestAttachment(1)79 ac := registryTestAttachment(1)
80- ac.size = domain.Size{Cols: 80, Rows: 23}80+ ac.geometry.Size = domain.Size{Cols: 80, Rows: 23}
81 ac.output = newOutputStateStream()81 ac.output = newOutputStateStream()
82 ac.output.attachment = ac82 ac.output.attachment = ac
83 require.True(t, sess.registerAttachment(ac))83 require.True(t, sess.registerAttachment(ac))
@@ -105,7 +105,7 @@ func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.
105 func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {105 func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {
106 sess := registryTestSession()106 sess := registryTestSession()
107 ac := registryTestAttachment(1)107 ac := registryTestAttachment(1)
108- ac.size = domain.Size{Cols: 80, Rows: 23}108+ ac.geometry.Size = domain.Size{Cols: 80, Rows: 23}
109 ac.output = newOutputStateStream()109 ac.output = newOutputStateStream()
110 ac.output.attachment = ac110 ac.output.attachment = ac
111 require.True(t, sess.registerAttachment(ac))111 require.True(t, sess.registerAttachment(ac))
M internal/usecase/daemon/attachment_render_test.go +7 -7
@@ -118,7 +118,7 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
118118 require.True(t, sess.repairAttachmentView(second))
119119
120120 tb := sess.tabs[0]
121- firstSize := first.size
121+ firstSize := first.geometry.Size
122122 secondRevision := second.viewSnapshot().revision
123123 secondEpoch := second.output.currentEpoch()
124124
@@ -136,8 +136,8 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
136136 tb.mu.Lock()
137137 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)
138138 tb.mu.Unlock()
139- require.Equal(t, firstSize, first.size)
140- require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.size)
139+ require.Equal(t, firstSize, first.geometry.Size)
140+ require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.geometry.Size)
141141 require.Equal(t, secondRevision+1, second.viewSnapshot().revision)
142142 require.Greater(t, second.output.currentEpoch(), secondEpoch)
143143 }
@@ -292,8 +292,8 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
292292 tb.mu.Lock()
293293 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)
294294 tb.mu.Unlock()
295- require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size)
296- require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.size)
295+ require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size)
296+ require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.geometry.Size)
297297
298298 // A same-size resize is still a claim, so the smaller peer can take
299299 // authority without changing its local window state first.
@@ -308,8 +308,8 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
308308 tb.mu.Lock()
309309 require.Equal(t, domain.Size{Cols: 70, Rows: 18}, tb.size)
310310 tb.mu.Unlock()
311- require.Equal(t, domain.Size{Cols: 70, Rows: 20}, first.size)
312- require.Equal(t, domain.Size{Cols: 90, Rows: 30}, second.size)
311+ require.Equal(t, domain.Size{Cols: 70, Rows: 20}, first.geometry.Size)
312+ require.Equal(t, domain.Size{Cols: 90, Rows: 30}, second.geometry.Size)
313313
314314 // Detaching the latest claimant falls back to the most recent remaining
315315 // attachment claim rather than reverting to the historical maximum.
@@ -118,7 +118,7 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
118 require.True(t, sess.repairAttachmentView(second))118 require.True(t, sess.repairAttachmentView(second))
119 119
120 tb := sess.tabs[0]120 tb := sess.tabs[0]
121- firstSize := first.size121+ firstSize := first.geometry.Size
122 secondRevision := second.viewSnapshot().revision122 secondRevision := second.viewSnapshot().revision
123 secondEpoch := second.output.currentEpoch()123 secondEpoch := second.output.currentEpoch()
124 124
@@ -136,8 +136,8 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
136 tb.mu.Lock()136 tb.mu.Lock()
137 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)137 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)
138 tb.mu.Unlock()138 tb.mu.Unlock()
139- require.Equal(t, firstSize, first.size)139+ require.Equal(t, firstSize, first.geometry.Size)
140- require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.size)140+ require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.geometry.Size)
141 require.Equal(t, secondRevision+1, second.viewSnapshot().revision)141 require.Equal(t, secondRevision+1, second.viewSnapshot().revision)
142 require.Greater(t, second.output.currentEpoch(), secondEpoch)142 require.Greater(t, second.output.currentEpoch(), secondEpoch)
143 }143 }
@@ -292,8 +292,8 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
292 tb.mu.Lock()292 tb.mu.Lock()
293 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)293 require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size)
294 tb.mu.Unlock()294 tb.mu.Unlock()
295- require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size)295+ require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size)
296- require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.size)296+ require.Equal(t, domain.Size{Cols: 120, Rows: 50}, second.geometry.Size)
297 297
298 // A same-size resize is still a claim, so the smaller peer can take298 // A same-size resize is still a claim, so the smaller peer can take
299 // authority without changing its local window state first.299 // authority without changing its local window state first.
@@ -308,8 +308,8 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
308 tb.mu.Lock()308 tb.mu.Lock()
309 require.Equal(t, domain.Size{Cols: 70, Rows: 18}, tb.size)309 require.Equal(t, domain.Size{Cols: 70, Rows: 18}, tb.size)
310 tb.mu.Unlock()310 tb.mu.Unlock()
311- require.Equal(t, domain.Size{Cols: 70, Rows: 20}, first.size)311+ require.Equal(t, domain.Size{Cols: 70, Rows: 20}, first.geometry.Size)
312- require.Equal(t, domain.Size{Cols: 90, Rows: 30}, second.size)312+ require.Equal(t, domain.Size{Cols: 90, Rows: 30}, second.geometry.Size)
313 313
314 // Detaching the latest claimant falls back to the most recent remaining314 // Detaching the latest claimant falls back to the most recent remaining
315 // attachment claim rather than reverting to the historical maximum.315 // attachment claim rather than reverting to the historical maximum.
M internal/usecase/daemon/attention.go +1 -4
@@ -19,10 +19,7 @@ func (d *Daemon) noteAttention(sess *session, tb *tab) {
1919 return
2020 }
2121
22- now := d.clock.Now()
23- if now.IsZero() {
24- now = time.Now()
25- }
22+ now := d.daemonNow()
2623
2724 sess.mu.Lock()
2825 if !tb.attention {
@@ -19,10 +19,7 @@ func (d *Daemon) noteAttention(sess *session, tb *tab) {
19 return19 return
20 }20 }
21 21
22- now := d.clock.Now()22+ now := d.daemonNow()
23- if now.IsZero() {
24- now = time.Now()
25- }
26 23
27 sess.mu.Lock()24 sess.mu.Lock()
28 if !tb.attention {25 if !tb.attention {
M internal/usecase/daemon/attention_test.go +1 -1
@@ -172,7 +172,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
172172 return nil
173173 }).Maybe()
174174 trW.EXPECT().Close().Return(nil).Maybe()
175- acW := &attachedClient{tr: trW, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
175+ acW := &attachedClient{tr: trW, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
176176 acW.initOverlays()
177177 sctxW, cancelW := context.WithCancel(d.serveCtx)
178178 t.Cleanup(cancelW)
@@ -172,7 +172,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
172 return nil172 return nil
173 }).Maybe()173 }).Maybe()
174 trW.EXPECT().Close().Return(nil).Maybe()174 trW.EXPECT().Close().Return(nil).Maybe()
175- acW := &attachedClient{tr: trW, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}175+ acW := &attachedClient{tr: trW, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
176 acW.initOverlays()176 acW.initOverlays()
177 sctxW, cancelW := context.WithCancel(d.serveCtx)177 sctxW, cancelW := context.WithCancel(d.serveCtx)
178 t.Cleanup(cancelW)178 t.Cleanup(cancelW)
M internal/usecase/daemon/bar_refresh_test.go +14 -14
@@ -64,7 +64,7 @@ func TestBarScriptRefreshIntervalClampingTickAndLastGoodOnFailure(t *testing.T)
6464 r := &fakeBarRunner{outs: []string{"top1", "bot1", "top2", "bot2"}}
6565 d := newBarRefreshTestDaemon(r, 100*time.Millisecond)
6666 sess := newBarRefreshTestSession()
67- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 120, Rows: 24}})
67+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 120, Rows: 24}}})
6868
6969 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))
7070 waitBarRefreshIdle(t, d)
@@ -91,7 +91,7 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
9191 sess := newBarRefreshTestSession()
9292 sess.name = "work"
9393 sess.cwd = "/repo"
94- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 132, Rows: 40}})
94+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 132, Rows: 40}}})
9595 sess.tabs[0].size = domain.Size{Cols: 132, Rows: 38}
9696 active := sess.tabs[0].focusedPane()
9797 active.stableID = "pane-default"
@@ -130,7 +130,7 @@ func TestBarScriptRefreshSkipsRunnerWhenCommandsDisabled(t *testing.T) {
130130 d.barScripts.cfg.topRight = ""
131131 d.barScripts.cfg.bottomRight = ""
132132 sess := newBarRefreshTestSession()
133- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
133+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
134134 d.barScripts.outputs[sess.id] = barScriptOutputs{topRight: "old", bottomRight: "old"}
135135
136136 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
@@ -145,7 +145,7 @@ func TestDefaultBarConfigDoesNotInvokeRunner(t *testing.T) {
145145 d := newBarRefreshTestDaemon(r, time.Second)
146146 d.barScripts.cfg = barConfigFromDomain(domain.Defaults().Bar)
147147 sess := newBarRefreshTestSession()
148- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
148+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
149149
150150 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
151151 require.Empty(t, r.calls)
@@ -155,7 +155,7 @@ func TestBarScriptForcedRefreshRespectsMinimumInterval(t *testing.T) {
155155 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
156156 d := newBarRefreshTestDaemon(r, 5*time.Second)
157157 sess := newBarRefreshTestSession()
158- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
158+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
159159
160160 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
161161 waitBarRefreshIdle(t, d)
@@ -172,7 +172,7 @@ func TestBarScriptContextChangeDebouncesUntilMinimumInterval(t *testing.T) {
172172 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
173173 d := newBarRefreshTestDaemon(r, 5*time.Second)
174174 sess := newBarRefreshTestSession()
175- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
175+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
176176
177177 require.True(t, d.refreshBarScriptsIfDue(sess, time.Now(), false))
178178 waitBarRefreshIdle(t, d)
@@ -190,7 +190,7 @@ func TestBarScriptRefreshForcesWhenContextChanges(t *testing.T) {
190190 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
191191 d := newBarRefreshTestDaemon(r, 5*time.Second)
192192 sess := newBarRefreshTestSession()
193- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
193+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
194194
195195 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))
196196 waitBarRefreshIdle(t, d)
@@ -218,7 +218,7 @@ func TestRefreshBarScriptsAllSessionsForcesRun(t *testing.T) {
218218 r := &fakeBarRunner{outs: []string{"top1", "bottom1"}}
219219 d := newBarRefreshTestDaemon(r, 60*time.Second)
220220 sess := newBarRefreshTestSession()
221- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
221+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
222222 d.sessions = map[domain.SessionID]*session{sess.id: sess}
223223
224224 // Verifies refreshBarScriptsAllSessions iterates every live session and
@@ -251,7 +251,7 @@ func TestBarScriptFailureLogsAndRetainsLastGood(t *testing.T) {
251251 d := newBarRefreshTestDaemon(r, time.Second)
252252 d.log = slog.New(slog.NewTextHandler(&logs, nil))
253253 sess := newBarRefreshTestSession()
254- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
254+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
255255
256256 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
257257 waitBarRefreshIdle(t, d)
@@ -286,7 +286,7 @@ func TestBarScriptRunConsumesPendingAfterRunningClears(t *testing.T) {
286286 r := newBlockingBarRunner([]string{"top1", "bottom1", "top2", "bottom2"})
287287 d := newBarRefreshTestDaemon(r, time.Second)
288288 sess := newBarRefreshTestSession()
289- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
289+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
290290
291291 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
292292 select {
@@ -336,7 +336,7 @@ func TestBarScriptFailureLogsOnceUntilSignatureChanges(t *testing.T) {
336336 d.log = logger
337337 d.baseEnv = []string{"PATH=/usr/bin"}
338338 sess := newBarRefreshTestSession()
339- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
339+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
340340
341341 readBuf := func() string {
342342 mu.Lock()
@@ -419,11 +419,11 @@ func TestBarScriptRefreshIsPerSession(t *testing.T) {
419419 a := newBarRefreshTestSession()
420420 a.id = "a"
421421 a.name = "a"
422- a.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
422+ a.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
423423 b := newBarRefreshTestSession()
424424 b.id = "b"
425425 b.name = "b"
426- b.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
426+ b.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
427427
428428 require.True(t, d.refreshBarScriptsIfDue(a, time.Unix(0, 0), false))
429429 waitBarRefreshIdle(t, d)
@@ -644,7 +644,7 @@ func TestBarScriptsUseSessionEnvNotDaemonBaseEnv(t *testing.T) {
644644 d.baseEnv = tc.baseEnv
645645 sess := newBarRefreshTestSession()
646646 sess.env = tc.sessEnv
647- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})
647+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
648648
649649 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
650650 waitBarRefreshIdle(t, d)
@@ -64,7 +64,7 @@ func TestBarScriptRefreshIntervalClampingTickAndLastGoodOnFailure(t *testing.T)
64 r := &fakeBarRunner{outs: []string{"top1", "bot1", "top2", "bot2"}}64 r := &fakeBarRunner{outs: []string{"top1", "bot1", "top2", "bot2"}}
65 d := newBarRefreshTestDaemon(r, 100*time.Millisecond)65 d := newBarRefreshTestDaemon(r, 100*time.Millisecond)
66 sess := newBarRefreshTestSession()66 sess := newBarRefreshTestSession()
67- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 120, Rows: 24}})67+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 120, Rows: 24}}})
68 68
69 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))69 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))
70 waitBarRefreshIdle(t, d)70 waitBarRefreshIdle(t, d)
@@ -91,7 +91,7 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
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{geometry: domain.Geometry{Size: domain.Size{Cols: 132, Rows: 40}}})
95 sess.tabs[0].size = domain.Size{Cols: 132, Rows: 38}95 sess.tabs[0].size = domain.Size{Cols: 132, Rows: 38}
96 active := sess.tabs[0].focusedPane()96 active := sess.tabs[0].focusedPane()
97 active.stableID = "pane-default"97 active.stableID = "pane-default"
@@ -130,7 +130,7 @@ func TestBarScriptRefreshSkipsRunnerWhenCommandsDisabled(t *testing.T) {
130 d.barScripts.cfg.topRight = ""130 d.barScripts.cfg.topRight = ""
131 d.barScripts.cfg.bottomRight = ""131 d.barScripts.cfg.bottomRight = ""
132 sess := newBarRefreshTestSession()132 sess := newBarRefreshTestSession()
133- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})133+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
134 d.barScripts.outputs[sess.id] = barScriptOutputs{topRight: "old", bottomRight: "old"}134 d.barScripts.outputs[sess.id] = barScriptOutputs{topRight: "old", bottomRight: "old"}
135 135
136 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))136 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
@@ -145,7 +145,7 @@ func TestDefaultBarConfigDoesNotInvokeRunner(t *testing.T) {
145 d := newBarRefreshTestDaemon(r, time.Second)145 d := newBarRefreshTestDaemon(r, time.Second)
146 d.barScripts.cfg = barConfigFromDomain(domain.Defaults().Bar)146 d.barScripts.cfg = barConfigFromDomain(domain.Defaults().Bar)
147 sess := newBarRefreshTestSession()147 sess := newBarRefreshTestSession()
148- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})148+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
149 149
150 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))150 require.False(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
151 require.Empty(t, r.calls)151 require.Empty(t, r.calls)
@@ -155,7 +155,7 @@ func TestBarScriptForcedRefreshRespectsMinimumInterval(t *testing.T) {
155 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}155 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
156 d := newBarRefreshTestDaemon(r, 5*time.Second)156 d := newBarRefreshTestDaemon(r, 5*time.Second)
157 sess := newBarRefreshTestSession()157 sess := newBarRefreshTestSession()
158- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})158+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
159 159
160 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))160 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
161 waitBarRefreshIdle(t, d)161 waitBarRefreshIdle(t, d)
@@ -172,7 +172,7 @@ func TestBarScriptContextChangeDebouncesUntilMinimumInterval(t *testing.T) {
172 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}172 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
173 d := newBarRefreshTestDaemon(r, 5*time.Second)173 d := newBarRefreshTestDaemon(r, 5*time.Second)
174 sess := newBarRefreshTestSession()174 sess := newBarRefreshTestSession()
175- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})175+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
176 176
177 require.True(t, d.refreshBarScriptsIfDue(sess, time.Now(), false))177 require.True(t, d.refreshBarScriptsIfDue(sess, time.Now(), false))
178 waitBarRefreshIdle(t, d)178 waitBarRefreshIdle(t, d)
@@ -190,7 +190,7 @@ func TestBarScriptRefreshForcesWhenContextChanges(t *testing.T) {
190 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}190 r := &fakeBarRunner{outs: []string{"top1", "bottom1", "top2", "bottom2"}}
191 d := newBarRefreshTestDaemon(r, 5*time.Second)191 d := newBarRefreshTestDaemon(r, 5*time.Second)
192 sess := newBarRefreshTestSession()192 sess := newBarRefreshTestSession()
193- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})193+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
194 194
195 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))195 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), false))
196 waitBarRefreshIdle(t, d)196 waitBarRefreshIdle(t, d)
@@ -218,7 +218,7 @@ func TestRefreshBarScriptsAllSessionsForcesRun(t *testing.T) {
218 r := &fakeBarRunner{outs: []string{"top1", "bottom1"}}218 r := &fakeBarRunner{outs: []string{"top1", "bottom1"}}
219 d := newBarRefreshTestDaemon(r, 60*time.Second)219 d := newBarRefreshTestDaemon(r, 60*time.Second)
220 sess := newBarRefreshTestSession()220 sess := newBarRefreshTestSession()
221- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})221+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
222 d.sessions = map[domain.SessionID]*session{sess.id: sess}222 d.sessions = map[domain.SessionID]*session{sess.id: sess}
223 223
224 // Verifies refreshBarScriptsAllSessions iterates every live session and224 // Verifies refreshBarScriptsAllSessions iterates every live session and
@@ -251,7 +251,7 @@ func TestBarScriptFailureLogsAndRetainsLastGood(t *testing.T) {
251 d := newBarRefreshTestDaemon(r, time.Second)251 d := newBarRefreshTestDaemon(r, time.Second)
252 d.log = slog.New(slog.NewTextHandler(&logs, nil))252 d.log = slog.New(slog.NewTextHandler(&logs, nil))
253 sess := newBarRefreshTestSession()253 sess := newBarRefreshTestSession()
254- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})254+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
255 255
256 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))256 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
257 waitBarRefreshIdle(t, d)257 waitBarRefreshIdle(t, d)
@@ -286,7 +286,7 @@ func TestBarScriptRunConsumesPendingAfterRunningClears(t *testing.T) {
286 r := newBlockingBarRunner([]string{"top1", "bottom1", "top2", "bottom2"})286 r := newBlockingBarRunner([]string{"top1", "bottom1", "top2", "bottom2"})
287 d := newBarRefreshTestDaemon(r, time.Second)287 d := newBarRefreshTestDaemon(r, time.Second)
288 sess := newBarRefreshTestSession()288 sess := newBarRefreshTestSession()
289- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})289+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
290 290
291 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))291 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(10, 0), true))
292 select {292 select {
@@ -336,7 +336,7 @@ func TestBarScriptFailureLogsOnceUntilSignatureChanges(t *testing.T) {
336 d.log = logger336 d.log = logger
337 d.baseEnv = []string{"PATH=/usr/bin"}337 d.baseEnv = []string{"PATH=/usr/bin"}
338 sess := newBarRefreshTestSession()338 sess := newBarRefreshTestSession()
339- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})339+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
340 340
341 readBuf := func() string {341 readBuf := func() string {
342 mu.Lock()342 mu.Lock()
@@ -419,11 +419,11 @@ func TestBarScriptRefreshIsPerSession(t *testing.T) {
419 a := newBarRefreshTestSession()419 a := newBarRefreshTestSession()
420 a.id = "a"420 a.id = "a"
421 a.name = "a"421 a.name = "a"
422- a.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})422+ a.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
423 b := newBarRefreshTestSession()423 b := newBarRefreshTestSession()
424 b.id = "b"424 b.id = "b"
425 b.name = "b"425 b.name = "b"
426- b.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})426+ b.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
427 427
428 require.True(t, d.refreshBarScriptsIfDue(a, time.Unix(0, 0), false))428 require.True(t, d.refreshBarScriptsIfDue(a, time.Unix(0, 0), false))
429 waitBarRefreshIdle(t, d)429 waitBarRefreshIdle(t, d)
@@ -644,7 +644,7 @@ func TestBarScriptsUseSessionEnvNotDaemonBaseEnv(t *testing.T) {
644 d.baseEnv = tc.baseEnv644 d.baseEnv = tc.baseEnv
645 sess := newBarRefreshTestSession()645 sess := newBarRefreshTestSession()
646 sess.env = tc.sessEnv646 sess.env = tc.sessEnv
647- sess.registerAttachment(&attachedClient{size: domain.Size{Cols: 80, Rows: 24}})647+ sess.registerAttachment(&attachedClient{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}})
648 648
649 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))649 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
650 waitBarRefreshIdle(t, d)650 waitBarRefreshIdle(t, d)
M internal/usecase/daemon/client.go +2 -6
@@ -94,7 +94,6 @@ type attachedClient struct {
9494 // sizeMu lets shared-geometry snapshots read attachment windows without
9595 // waiting behind a blocked transport send.
9696 sizeMu sync.RWMutex
97- size domain.Size // retained for legacy fixtures; live paths publish geometry
9897 geometry domain.Geometry // controlling-terminal geometry for this Attachment
9998 keys *keys.Router
10099 // view is attachment-local navigation state. It is never inferred from a
@@ -181,7 +180,7 @@ func (ac *attachedClient) sizeSnapshot() domain.Size {
181180 }
182181 ac.sizeMu.RLock()
183182 defer ac.sizeMu.RUnlock()
184- return ac.size
183+ return ac.geometry.Size
185184 }
186185
187186 func (ac *attachedClient) setSize(size domain.Size) {
@@ -189,7 +188,6 @@ func (ac *attachedClient) setSize(size domain.Size) {
189188 return
190189 }
191190 ac.sizeMu.Lock()
192- ac.size = size
193191 ac.geometry.Size = size
194192 ac.geometry = ac.geometry.NormalizePixels()
195193 ac.sizeMu.Unlock()
@@ -204,7 +202,7 @@ func (ac *attachedClient) geometrySnapshot() domain.Geometry {
204202 if ac.geometry.Valid() {
205203 return ac.geometry
206204 }
207- return domain.Geometry{Size: ac.size}
205+ return domain.Geometry{Size: ac.geometry.Size}
208206 }
209207
210208 func (ac *attachedClient) setGeometry(geometry domain.Geometry) {
@@ -213,7 +211,6 @@ func (ac *attachedClient) setGeometry(geometry domain.Geometry) {
213211 }
214212 geometry = geometry.NormalizePixels()
215213 ac.sizeMu.Lock()
216- ac.size = geometry.Size
217214 ac.geometry = geometry
218215 ac.sizeMu.Unlock()
219216 }
@@ -772,7 +769,6 @@ func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.ServerConne
772769 ac := &attachedClient{
773770 tr: tr,
774771 output: output,
775- size: geometry.Size,
776772 geometry: geometry,
777773 view: attachmentView{windowRows: geometry.Rows, windowSet: true},
778774 clientID: opts.clientID,
@@ -94,7 +94,6 @@ type attachedClient struct {
94 // sizeMu lets shared-geometry snapshots read attachment windows without94 // sizeMu lets shared-geometry snapshots read attachment windows without
95 // waiting behind a blocked transport send.95 // waiting behind a blocked transport send.
96 sizeMu sync.RWMutex96 sizeMu sync.RWMutex
97- size domain.Size // retained for legacy fixtures; live paths publish geometry
98 geometry domain.Geometry // controlling-terminal geometry for this Attachment97 geometry domain.Geometry // controlling-terminal geometry for this Attachment
99 keys *keys.Router98 keys *keys.Router
100 // view is attachment-local navigation state. It is never inferred from a99 // view is attachment-local navigation state. It is never inferred from a
@@ -181,7 +180,7 @@ func (ac *attachedClient) sizeSnapshot() domain.Size {
181 }180 }
182 ac.sizeMu.RLock()181 ac.sizeMu.RLock()
183 defer ac.sizeMu.RUnlock()182 defer ac.sizeMu.RUnlock()
184- return ac.size183+ return ac.geometry.Size
185 }184 }
186 185
187 func (ac *attachedClient) setSize(size domain.Size) {186 func (ac *attachedClient) setSize(size domain.Size) {
@@ -189,7 +188,6 @@ func (ac *attachedClient) setSize(size domain.Size) {
189 return188 return
190 }189 }
191 ac.sizeMu.Lock()190 ac.sizeMu.Lock()
192- ac.size = size
193 ac.geometry.Size = size191 ac.geometry.Size = size
194 ac.geometry = ac.geometry.NormalizePixels()192 ac.geometry = ac.geometry.NormalizePixels()
195 ac.sizeMu.Unlock()193 ac.sizeMu.Unlock()
@@ -204,7 +202,7 @@ func (ac *attachedClient) geometrySnapshot() domain.Geometry {
204 if ac.geometry.Valid() {202 if ac.geometry.Valid() {
205 return ac.geometry203 return ac.geometry
206 }204 }
207- return domain.Geometry{Size: ac.size}205+ return domain.Geometry{Size: ac.geometry.Size}
208 }206 }
209 207
210 func (ac *attachedClient) setGeometry(geometry domain.Geometry) {208 func (ac *attachedClient) setGeometry(geometry domain.Geometry) {
@@ -213,7 +211,6 @@ func (ac *attachedClient) setGeometry(geometry domain.Geometry) {
213 }211 }
214 geometry = geometry.NormalizePixels()212 geometry = geometry.NormalizePixels()
215 ac.sizeMu.Lock()213 ac.sizeMu.Lock()
216- ac.size = geometry.Size
217 ac.geometry = geometry214 ac.geometry = geometry
218 ac.sizeMu.Unlock()215 ac.sizeMu.Unlock()
219 }216 }
@@ -772,7 +769,6 @@ func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.ServerConne
772 ac := &attachedClient{769 ac := &attachedClient{
773 tr: tr,770 tr: tr,
774 output: output,771 output: output,
775- size: geometry.Size,
776 geometry: geometry,772 geometry: geometry,
777 view: attachmentView{windowRows: geometry.Rows, windowSet: true},773 view: attachmentView{windowRows: geometry.Rows, windowSet: true},
778 clientID: opts.clientID,774 clientID: opts.clientID,
M internal/usecase/daemon/copy_scroll_ownership_test.go +2 -1
@@ -4,6 +4,7 @@ import (
44 "testing"
55
66 renderer "github.com/bnema/vev-vt"
7+ "github.com/bnema/vev/internal/domain"
78 "github.com/bnema/vev/internal/protocol"
89 scopy "github.com/bnema/vev/internal/usecase/copy"
910 "github.com/stretchr/testify/require"
@@ -94,7 +95,7 @@ func TestCopyExitRefreshesAfterOtherAttachmentConsumesDamage(t *testing.T) {
9495 copyBefore := captureTestFrame(ac.render.cache.frame)
9596
9697 tr, peerSends := newCapturingTransport(t)
97- peer := &attachedClient{tr: tr, output: newOutputStateStream(), size: ac.size}
98+ peer := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
9899 peer.output.attachment = peer
99100 peer.initOverlays()
100101 peer.setSession(sess)
@@ -4,6 +4,7 @@ import (
4 "testing"4 "testing"
5 5
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7+ "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/protocol"8 "github.com/bnema/vev/internal/protocol"
8 scopy "github.com/bnema/vev/internal/usecase/copy"9 scopy "github.com/bnema/vev/internal/usecase/copy"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
@@ -94,7 +95,7 @@ func TestCopyExitRefreshesAfterOtherAttachmentConsumesDamage(t *testing.T) {
94 copyBefore := captureTestFrame(ac.render.cache.frame)95 copyBefore := captureTestFrame(ac.render.cache.frame)
95 96
96 tr, peerSends := newCapturingTransport(t)97 tr, peerSends := newCapturingTransport(t)
97- peer := &attachedClient{tr: tr, output: newOutputStateStream(), size: ac.size}98+ peer := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
98 peer.output.attachment = peer99 peer.output.attachment = peer
99 peer.initOverlays()100 peer.initOverlays()
100 peer.setSession(sess)101 peer.setSession(sess)
M internal/usecase/daemon/daemon_test.go +4 -3
@@ -26,10 +26,11 @@ import (
2626
2727 // stubClock returns timers whose channel never fires, so a scheduler under it
2828 // blocks in its debounce loop until the session context is cancelled. Used by
29-// every test that is not specifically exercising the debounce.
29+// every test that is not specifically exercising the debounce. Now reports a
30+// fixed non-zero instant so recorded timestamps are deterministic and set.
3031 type stubClock struct{}
3132
32-func (stubClock) Now() time.Time { return time.Time{} }
33+func (stubClock) Now() time.Time { return time.Unix(1_700_000_000, 0) }
3334 func (stubClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} }
3435
3536 type stubTimer struct{}
@@ -416,7 +417,7 @@ func newManualSessionWithPTYsClockCleanup(t testing.TB, clock ports.Clock, regis
416417 t.Helper()
417418 d := newTestDaemonWithCleanup(t, nil, clock, registerCleanup)
418419 tr, sends := newCapturingTransport(t)
419- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
420+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
420421 ac.output.attachment = ac
421422 ac.initOverlays()
422423 sctx, cancel := context.WithCancel(d.serveCtx)
@@ -26,10 +26,11 @@ import (
26 26
27 // stubClock returns timers whose channel never fires, so a scheduler under it27 // stubClock returns timers whose channel never fires, so a scheduler under it
28 // blocks in its debounce loop until the session context is cancelled. Used by28 // blocks in its debounce loop until the session context is cancelled. Used by
29-// every test that is not specifically exercising the debounce.29+// every test that is not specifically exercising the debounce. Now reports a
30+// fixed non-zero instant so recorded timestamps are deterministic and set.
30 type stubClock struct{}31 type stubClock struct{}
31 32
32-func (stubClock) Now() time.Time { return time.Time{} }33+func (stubClock) Now() time.Time { return time.Unix(1_700_000_000, 0) }
33 func (stubClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} }34 func (stubClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} }
34 35
35 type stubTimer struct{}36 type stubTimer struct{}
@@ -416,7 +417,7 @@ func newManualSessionWithPTYsClockCleanup(t testing.TB, clock ports.Clock, regis
416 t.Helper()417 t.Helper()
417 d := newTestDaemonWithCleanup(t, nil, clock, registerCleanup)418 d := newTestDaemonWithCleanup(t, nil, clock, registerCleanup)
418 tr, sends := newCapturingTransport(t)419 tr, sends := newCapturingTransport(t)
419- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}420+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
420 ac.output.attachment = ac421 ac.output.attachment = ac
421 ac.initOverlays()422 ac.initOverlays()
422 sctx, cancel := context.WithCancel(d.serveCtx)423 sctx, cancel := context.WithCancel(d.serveCtx)
M internal/usecase/daemon/final_acceptance_test.go +9 -9
@@ -19,9 +19,9 @@ func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *test
1919
2020 secondTransport, _ := newCapturingTransport(t)
2121 second := &attachedClient{
22- tr: secondTransport,
23- output: newOutputStateStream(),
24- size: domain.Size{Cols: 80, Rows: 24},
22+ tr: secondTransport,
23+ output: newOutputStateStream(),
24+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
2525 }
2626 second.initOverlays()
2727 second.clientID[0] = 2
@@ -85,7 +85,7 @@ func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *test
8585 first.sendMu.Unlock()
8686 require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary")
8787 second.sendMu.Lock()
88- gotSecondSize := second.size
88+ gotSecondSize := second.geometry.Size
8989 second.sendMu.Unlock()
9090 require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize)
9191 }
@@ -99,9 +99,9 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
9999
100100 secondTransport, _ := newCapturingTransport(t)
101101 second := &attachedClient{
102- tr: secondTransport,
103- output: newOutputStateStream(),
104- size: domain.Size{Cols: 80, Rows: 24},
102+ tr: secondTransport,
103+ output: newOutputStateStream(),
104+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
105105 }
106106 second.initOverlays()
107107 second.clientID[0] = 2
@@ -156,8 +156,8 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
156156 require.True(t, admitted)
157157 require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
158158 secondEffect.End()
159- require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.size)
160- require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size, "peer resize crossed attachment boundary")
159+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.geometry.Size)
160+ require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size, "peer resize crossed attachment boundary")
161161 sess.mu.Lock()
162162 tb := sess.tabs[0]
163163 sess.mu.Unlock()
@@ -19,9 +19,9 @@ func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *test
19 19
20 secondTransport, _ := newCapturingTransport(t)20 secondTransport, _ := newCapturingTransport(t)
21 second := &attachedClient{21 second := &attachedClient{
22- tr: secondTransport,22+ tr: secondTransport,
23- output: newOutputStateStream(),23+ output: newOutputStateStream(),
24- size: domain.Size{Cols: 80, Rows: 24},24+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
25 }25 }
26 second.initOverlays()26 second.initOverlays()
27 second.clientID[0] = 227 second.clientID[0] = 2
@@ -85,7 +85,7 @@ func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *test
85 first.sendMu.Unlock()85 first.sendMu.Unlock()
86 require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary")86 require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary")
87 second.sendMu.Lock()87 second.sendMu.Lock()
88- gotSecondSize := second.size88+ gotSecondSize := second.geometry.Size
89 second.sendMu.Unlock()89 second.sendMu.Unlock()
90 require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize)90 require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize)
91 }91 }
@@ -99,9 +99,9 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
99 99
100 secondTransport, _ := newCapturingTransport(t)100 secondTransport, _ := newCapturingTransport(t)
101 second := &attachedClient{101 second := &attachedClient{
102- tr: secondTransport,102+ tr: secondTransport,
103- output: newOutputStateStream(),103+ output: newOutputStateStream(),
104- size: domain.Size{Cols: 80, Rows: 24},104+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
105 }105 }
106 second.initOverlays()106 second.initOverlays()
107 second.clientID[0] = 2107 second.clientID[0] = 2
@@ -156,8 +156,8 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
156 require.True(t, admitted)156 require.True(t, admitted)
157 require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))157 require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
158 secondEffect.End()158 secondEffect.End()
159- require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.size)159+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.geometry.Size)
160- require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size, "peer resize crossed attachment boundary")160+ require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size, "peer resize crossed attachment boundary")
161 sess.mu.Lock()161 sess.mu.Lock()
162 tb := sess.tabs[0]162 tb := sess.tabs[0]
163 sess.mu.Unlock()163 sess.mu.Unlock()
M internal/usecase/daemon/floating_open_test.go +1 -1
@@ -226,7 +226,7 @@ func TestFloatingOpenErrorReleasesReturnedPTYBeforePublishingFailure(t *testing.
226226 d := newTestDaemon(t, factory, stubClock{})
227227 tb := newFloatingTestTab(t)
228228 tr, _ := newCapturingTransport(t)
229- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
229+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
230230 ac.initOverlays()
231231 sessCtx, cancelSession := context.WithCancel(t.Context())
232232 sess := &session{sessionCore: sessionCore{id: "floating-open-error", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: sessCtx, cancel: cancelSession}
@@ -226,7 +226,7 @@ func TestFloatingOpenErrorReleasesReturnedPTYBeforePublishingFailure(t *testing.
226 d := newTestDaemon(t, factory, stubClock{})226 d := newTestDaemon(t, factory, stubClock{})
227 tb := newFloatingTestTab(t)227 tb := newFloatingTestTab(t)
228 tr, _ := newCapturingTransport(t)228 tr, _ := newCapturingTransport(t)
229- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}229+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
230 ac.initOverlays()230 ac.initOverlays()
231 sessCtx, cancelSession := context.WithCancel(t.Context())231 sessCtx, cancelSession := context.WithCancel(t.Context())
232 sess := &session{sessionCore: sessionCore{id: "floating-open-error", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: sessCtx, cancel: cancelSession}232 sess := &session{sessionCore: sessionCore{id: "floating-open-error", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: sessCtx, cancel: cancelSession}
M internal/usecase/daemon/floating_test.go +1 -1
@@ -768,7 +768,7 @@ func TestFloatingAsyncSpawnFailureToastsOnlyForUserOpen(t *testing.T) {
768768 d := newTestDaemon(t, factory, stubClock{})
769769 tb := newFloatingTestTab(t)
770770 tr, _ := newCapturingTransport(t)
771- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
771+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
772772 ac.initOverlays()
773773 sess := &session{sessionCore: sessionCore{id: "floating-spawn", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: t.Context()}
774774 ac.setSession(sess)
@@ -768,7 +768,7 @@ func TestFloatingAsyncSpawnFailureToastsOnlyForUserOpen(t *testing.T) {
768 d := newTestDaemon(t, factory, stubClock{})768 d := newTestDaemon(t, factory, stubClock{})
769 tb := newFloatingTestTab(t)769 tb := newFloatingTestTab(t)
770 tr, _ := newCapturingTransport(t)770 tr, _ := newCapturingTransport(t)
771- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}771+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
772 ac.initOverlays()772 ac.initOverlays()
773 sess := &session{sessionCore: sessionCore{id: "floating-spawn", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: t.Context()}773 sess := &session{sessionCore: sessionCore{id: "floating-spawn", name: "work", attachments: map[*attachedClient]struct{}{ac: {}}}, tabs: []*tab{tb}, ctx: t.Context()}
774 ac.setSession(sess)774 ac.setSession(sess)
M internal/usecase/daemon/input_test.go +1 -1
@@ -191,7 +191,7 @@ func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) {
191191 d.paint(sess, ac, true, nil)
192192 first := awaitFrame(t, sends, "Output")
193193 firstOutput := unmarshalTestOutput(t, first.Payload)
194- terminal := vt.NewScreen(ac.size.Cols, ac.size.Rows)
194+ terminal := vt.NewScreen(ac.geometry.Size.Cols, ac.geometry.Size.Rows)
195195 terminal.Write(firstOutput.Data)
196196
197197 // A clean pane relies on the attachment capture cache for its retained
@@ -191,7 +191,7 @@ func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) {
191 d.paint(sess, ac, true, nil)191 d.paint(sess, ac, true, nil)
192 first := awaitFrame(t, sends, "Output")192 first := awaitFrame(t, sends, "Output")
193 firstOutput := unmarshalTestOutput(t, first.Payload)193 firstOutput := unmarshalTestOutput(t, first.Payload)
194- terminal := vt.NewScreen(ac.size.Cols, ac.size.Rows)194+ terminal := vt.NewScreen(ac.geometry.Size.Cols, ac.geometry.Size.Rows)
195 terminal.Write(firstOutput.Data)195 terminal.Write(firstOutput.Data)
196 196
197 // A clean pane relies on the attachment capture cache for its retained197 // A clean pane relies on the attachment capture cache for its retained
M internal/usecase/daemon/move_cleanup_test.go +1 -1
@@ -62,7 +62,7 @@ func TestMovePaneCleanupUsesCapturedSourceAttachment(t *testing.T) {
6262
6363 replacementTransport, _ := newCapturingTransport(t)
6464 replacement := &attachedClient{
65- tr: replacementTransport, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24},
65+ tr: replacementTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
6666 }
6767 replacement.initOverlays()
6868 replacement.setSession(source)
@@ -62,7 +62,7 @@ func TestMovePaneCleanupUsesCapturedSourceAttachment(t *testing.T) {
62 62
63 replacementTransport, _ := newCapturingTransport(t)63 replacementTransport, _ := newCapturingTransport(t)
64 replacement := &attachedClient{64 replacement := &attachedClient{
65- tr: replacementTransport, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24},65+ tr: replacementTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
66 }66 }
67 replacement.initOverlays()67 replacement.initOverlays()
68 replacement.setSession(source)68 replacement.setSession(source)
M internal/usecase/daemon/move_gate_race_test.go +1 -1
@@ -186,7 +186,7 @@ func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) {
186186 d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY())
187187 d.attachCoordinator(sess, nil, current, true)
188188 otherTransport := &closeTrackingTransport{}
189- other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), size: current.size}
189+ other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: current.geometry.Size}}
190190 other.initOverlays()
191191 other.setSession(sess)
192192 sess.mu.Lock()
@@ -186,7 +186,7 @@ func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) {
186 d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY())186 d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY())
187 d.attachCoordinator(sess, nil, current, true)187 d.attachCoordinator(sess, nil, current, true)
188 otherTransport := &closeTrackingTransport{}188 otherTransport := &closeTrackingTransport{}
189- other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), size: current.size}189+ other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: current.geometry.Size}}
190 other.initOverlays()190 other.initOverlays()
191 other.setSession(sess)191 other.setSession(sess)
192 sess.mu.Lock()192 sess.mu.Lock()
M internal/usecase/daemon/move_picker_test.go +2 -2
@@ -390,10 +390,10 @@ func TestMovePickerCompositeFollow(t *testing.T) {
390390 source.tabs = append(source.tabs, newTabWithStableID("remaining", "remaining-pane", pty, movedTab.size))
391391 publishTiledPaneOwners(source, source.tabs[1])
392392 }
393- peer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: ac.size}
393+ peer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
394394 peer.setSession(source)
395395 source.registerAttachment(peer)
396- destinationPeer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: ac.size}
396+ destinationPeer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
397397 destinationPeer.setSession(destination)
398398 destination.registerAttachment(destinationPeer)
399399 destination.selectAttachmentTabLocked(destinationPeer, domain.TabStableID(destinationTab.stableID))
@@ -390,10 +390,10 @@ func TestMovePickerCompositeFollow(t *testing.T) {
390 source.tabs = append(source.tabs, newTabWithStableID("remaining", "remaining-pane", pty, movedTab.size))390 source.tabs = append(source.tabs, newTabWithStableID("remaining", "remaining-pane", pty, movedTab.size))
391 publishTiledPaneOwners(source, source.tabs[1])391 publishTiledPaneOwners(source, source.tabs[1])
392 }392 }
393- peer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: ac.size}393+ peer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
394 peer.setSession(source)394 peer.setSession(source)
395 source.registerAttachment(peer)395 source.registerAttachment(peer)
396- destinationPeer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: ac.size}396+ destinationPeer := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
397 destinationPeer.setSession(destination)397 destinationPeer.setSession(destination)
398 destination.registerAttachment(destinationPeer)398 destination.registerAttachment(destinationPeer)
399 destination.selectAttachmentTabLocked(destinationPeer, domain.TabStableID(destinationTab.stableID))399 destination.selectAttachmentTabLocked(destinationPeer, domain.TabStableID(destinationTab.stableID))
M internal/usecase/daemon/move_test.go +1 -1
@@ -142,7 +142,7 @@ func TestMovePaneRetiresSourceParkedClients(t *testing.T) {
142142 movedTab.stableID = "source-tab"
143143
144144 transport := &closeTrackingTransport{}
145- parked := &attachedClient{tr: transport, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}, resumeCapable: true}
145+ parked := &attachedClient{tr: transport, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, resumeCapable: true}
146146 parked.initOverlays()
147147 require.True(t, d.parkAttachment(source, parked))
148148 destinationPTY := newQuietPTY()
@@ -142,7 +142,7 @@ func TestMovePaneRetiresSourceParkedClients(t *testing.T) {
142 movedTab.stableID = "source-tab"142 movedTab.stableID = "source-tab"
143 143
144 transport := &closeTrackingTransport{}144 transport := &closeTrackingTransport{}
145- parked := &attachedClient{tr: transport, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}, resumeCapable: true}145+ parked := &attachedClient{tr: transport, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, resumeCapable: true}
146 parked.initOverlays()146 parked.initOverlays()
147 require.True(t, d.parkAttachment(source, parked))147 require.True(t, d.parkAttachment(source, parked))
148 destinationPTY := newQuietPTY()148 destinationPTY := newQuietPTY()
M internal/usecase/daemon/navigation_attachment_handoff_test.go +1 -1
@@ -172,7 +172,7 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.
172172 t.Fatal("stopped-session handoff did not release its role ticket")
173173 }
174174
175- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}
175+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
176176 next.initOverlays()
177177 replacement, err := d.transitionAttachment(attachmentTransitionRequest{
178178 target: source, next: next, expectedTransport: next.transportSnapshot(), ready: true,
@@ -172,7 +172,7 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.
172 t.Fatal("stopped-session handoff did not release its role ticket")172 t.Fatal("stopped-session handoff did not release its role ticket")
173 }173 }
174 174
175- next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), size: old.size}175+ next := &attachedClient{tr: &closeTrackingTransport{}, output: newOutputStateStream(), geometry: domain.Geometry{Size: old.geometry.Size}}
176 next.initOverlays()176 next.initOverlays()
177 replacement, err := d.transitionAttachment(attachmentTransitionRequest{177 replacement, err := d.transitionAttachment(attachmentTransitionRequest{
178 target: source, next: next, expectedTransport: next.transportSnapshot(), ready: true,178 target: source, next: next, expectedTransport: next.transportSnapshot(), ready: true,
M internal/usecase/daemon/notice_render_test.go +3 -3
@@ -81,14 +81,14 @@ func containsTopRight(rows []string, text string, minCol int) bool {
8181 func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {
8282 clk := newNoticeClock()
8383 d, sess, ac, sends := newNoticeFixture(t, clk)
84- screen := vt.NewScreen(ac.size.Cols, ac.size.Rows)
84+ screen := vt.NewScreen(ac.geometry.Size.Cols, ac.geometry.Size.Rows)
8585
8686 d.notify(sess, domain.NoticeError, domain.NoticePaneSpawn, "couldn't open pane", nil)
8787 awaitToastCount(t, ac, 1)
8888 replayOutputFrames(t, screen, drainAllFrames(sends))
8989
9090 rows := frameRows(captureTestFrame(screen))
91- if !containsTopRight(rows, "couldn't open pane", ac.size.Cols/2) {
91+ if !containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
9292 t.Fatalf("composed frame missing toast message top-right:\n%s", strings.Join(rows, "\n"))
9393 }
9494
@@ -102,7 +102,7 @@ func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {
102102 }
103103
104104 rows = frameRows(captureTestFrame(screen))
105- if containsTopRight(rows, "couldn't open pane", ac.size.Cols/2) {
105+ if containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
106106 t.Fatalf("toast message still present after TTL expired:\n%s", strings.Join(rows, "\n"))
107107 }
108108 }
@@ -81,14 +81,14 @@ func containsTopRight(rows []string, text string, minCol int) bool {
81 func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {81 func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {
82 clk := newNoticeClock()82 clk := newNoticeClock()
83 d, sess, ac, sends := newNoticeFixture(t, clk)83 d, sess, ac, sends := newNoticeFixture(t, clk)
84- screen := vt.NewScreen(ac.size.Cols, ac.size.Rows)84+ screen := vt.NewScreen(ac.geometry.Size.Cols, ac.geometry.Size.Rows)
85 85
86 d.notify(sess, domain.NoticeError, domain.NoticePaneSpawn, "couldn't open pane", nil)86 d.notify(sess, domain.NoticeError, domain.NoticePaneSpawn, "couldn't open pane", nil)
87 awaitToastCount(t, ac, 1)87 awaitToastCount(t, ac, 1)
88 replayOutputFrames(t, screen, drainAllFrames(sends))88 replayOutputFrames(t, screen, drainAllFrames(sends))
89 89
90 rows := frameRows(captureTestFrame(screen))90 rows := frameRows(captureTestFrame(screen))
91- if !containsTopRight(rows, "couldn't open pane", ac.size.Cols/2) {91+ if !containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
92 t.Fatalf("composed frame missing toast message top-right:\n%s", strings.Join(rows, "\n"))92 t.Fatalf("composed frame missing toast message top-right:\n%s", strings.Join(rows, "\n"))
93 }93 }
94 94
@@ -102,7 +102,7 @@ func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {
102 }102 }
103 103
104 rows = frameRows(captureTestFrame(screen))104 rows = frameRows(captureTestFrame(screen))
105- if containsTopRight(rows, "couldn't open pane", ac.size.Cols/2) {105+ if containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
106 t.Fatalf("toast message still present after TTL expired:\n%s", strings.Join(rows, "\n"))106 t.Fatalf("toast message still present after TTL expired:\n%s", strings.Join(rows, "\n"))
107 }107 }
108 }108 }
M internal/usecase/daemon/notify_test.go +5 -5
@@ -278,7 +278,7 @@ func newNoticeFixture(t *testing.T, clk ports.Clock) (*Daemon, *session, *attach
278278 p, _ := newBlockingPTY(t)
279279 d := newTestDaemon(t, nil, clk)
280280 tr, sends := newCapturingTransport(t)
281- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
281+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
282282 ac.initOverlays()
283283 sctx, cancel := context.WithCancel(d.serveCtx)
284284 wctx, wcancel := context.WithCancel(sctx)
@@ -443,7 +443,7 @@ func TestReportErrorNonBenignIsNeverDropped(t *testing.T) {
443443 func TestNotifyRoutesToSessionClientOnly(t *testing.T) {
444444 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())
445445
446- other := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
446+ other := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
447447 other.initOverlays()
448448 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{other: {}}}, ctx: sess.ctx, cancel: func() {}}
449449 other.setSession(sess2)
@@ -533,7 +533,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
533533 clearAttachmentsForTestLocked(sessA)
534534 sessA.mu.Unlock()
535535
536- clientB := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
536+ clientB := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
537537 clientB.initOverlays()
538538 sessB := &session{sessionCore: sessionCore{id: "manual-2", name: "other"}, ctx: sessA.ctx, cancel: func() {}}
539539 clientB.setSession(sessB)
@@ -572,7 +572,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
572572 require.Equal(t, "hello from a script", aToasts[0].Message)
573573 require.Equal(t, 1, aToasts[0].Count)
574574
575- secondA := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
575+ secondA := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
576576 secondA.initOverlays()
577577 secondA.setSession(sessA)
578578 sessA.mu.Lock()
@@ -586,7 +586,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
586586 func TestNotifyGlobalFansOutToAttachedClients(t *testing.T) {
587587 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())
588588
589- second := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
589+ second := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
590590 second.initOverlays()
591591 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{second: {}}}, ctx: sess.ctx, cancel: func() {}}
592592 second.setSession(sess2)
@@ -278,7 +278,7 @@ func newNoticeFixture(t *testing.T, clk ports.Clock) (*Daemon, *session, *attach
278 p, _ := newBlockingPTY(t)278 p, _ := newBlockingPTY(t)
279 d := newTestDaemon(t, nil, clk)279 d := newTestDaemon(t, nil, clk)
280 tr, sends := newCapturingTransport(t)280 tr, sends := newCapturingTransport(t)
281- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}281+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
282 ac.initOverlays()282 ac.initOverlays()
283 sctx, cancel := context.WithCancel(d.serveCtx)283 sctx, cancel := context.WithCancel(d.serveCtx)
284 wctx, wcancel := context.WithCancel(sctx)284 wctx, wcancel := context.WithCancel(sctx)
@@ -443,7 +443,7 @@ func TestReportErrorNonBenignIsNeverDropped(t *testing.T) {
443 func TestNotifyRoutesToSessionClientOnly(t *testing.T) {443 func TestNotifyRoutesToSessionClientOnly(t *testing.T) {
444 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())444 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())
445 445
446- other := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}446+ other := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
447 other.initOverlays()447 other.initOverlays()
448 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{other: {}}}, ctx: sess.ctx, cancel: func() {}}448 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{other: {}}}, ctx: sess.ctx, cancel: func() {}}
449 other.setSession(sess2)449 other.setSession(sess2)
@@ -533,7 +533,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
533 clearAttachmentsForTestLocked(sessA)533 clearAttachmentsForTestLocked(sessA)
534 sessA.mu.Unlock()534 sessA.mu.Unlock()
535 535
536- clientB := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}536+ clientB := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
537 clientB.initOverlays()537 clientB.initOverlays()
538 sessB := &session{sessionCore: sessionCore{id: "manual-2", name: "other"}, ctx: sessA.ctx, cancel: func() {}}538 sessB := &session{sessionCore: sessionCore{id: "manual-2", name: "other"}, ctx: sessA.ctx, cancel: func() {}}
539 clientB.setSession(sessB)539 clientB.setSession(sessB)
@@ -572,7 +572,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
572 require.Equal(t, "hello from a script", aToasts[0].Message)572 require.Equal(t, "hello from a script", aToasts[0].Message)
573 require.Equal(t, 1, aToasts[0].Count)573 require.Equal(t, 1, aToasts[0].Count)
574 574
575- secondA := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}575+ secondA := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
576 secondA.initOverlays()576 secondA.initOverlays()
577 secondA.setSession(sessA)577 secondA.setSession(sessA)
578 sessA.mu.Lock()578 sessA.mu.Lock()
@@ -586,7 +586,7 @@ func TestSessionScopedNoticeQueuedWhileDetached(t *testing.T) {
586 func TestNotifyGlobalFansOutToAttachedClients(t *testing.T) {586 func TestNotifyGlobalFansOutToAttachedClients(t *testing.T) {
587 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())587 d, sess, ac, _ := newNoticeFixture(t, newNoticeClock())
588 588
589- second := &attachedClient{output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}589+ second := &attachedClient{output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
590 second.initOverlays()590 second.initOverlays()
591 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{second: {}}}, ctx: sess.ctx, cancel: func() {}}591 sess2 := &session{sessionCore: sessionCore{id: "manual-2", name: "other", attachments: map[*attachedClient]struct{}{second: {}}}, ctx: sess.ctx, cancel: func() {}}
592 second.setSession(sess2)592 second.setSession(sess2)
M internal/usecase/daemon/output_state_sync_test.go +2 -2
@@ -358,7 +358,7 @@ func TestResizeWithoutPTYOutputFlushesOneFullFrameAtDeadline(t *testing.T) {
358358 require.Zero(t, out.Base)
359359 require.Contains(t, screenLineText(client, 0), "1")
360360 require.Contains(t, screenLineText(client, 23), "work")
361- require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)
361+ require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.geometry.Size)
362362 requireNoOutputFrame(t, sends)
363363 }
364364
@@ -392,7 +392,7 @@ func TestResizeBurstFlushesOnlyLatestGeometry(t *testing.T) {
392392 latest.ch <- time.Now()
393393 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")
394394 awaitFrame(t, sends, "Output")
395- require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)
395+ require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.geometry.Size)
396396 require.Equal(t, 120, testAttachmentTab(sess).focusedPane().screen.Columns())
397397 requireNoOutputFrame(t, sends)
398398 }
@@ -358,7 +358,7 @@ func TestResizeWithoutPTYOutputFlushesOneFullFrameAtDeadline(t *testing.T) {
358 require.Zero(t, out.Base)358 require.Zero(t, out.Base)
359 require.Contains(t, screenLineText(client, 0), "1")359 require.Contains(t, screenLineText(client, 0), "1")
360 require.Contains(t, screenLineText(client, 23), "work")360 require.Contains(t, screenLineText(client, 23), "work")
361- require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)361+ require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.geometry.Size)
362 requireNoOutputFrame(t, sends)362 requireNoOutputFrame(t, sends)
363 }363 }
364 364
@@ -392,7 +392,7 @@ func TestResizeBurstFlushesOnlyLatestGeometry(t *testing.T) {
392 latest.ch <- time.Now()392 latest.ch <- time.Now()
393 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")393 awaitTestCompletion(t, latestDone, "latest resize callback did not complete")
394 awaitFrame(t, sends, "Output")394 awaitFrame(t, sends, "Output")
395- require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.size)395+ require.Equal(t, domain.Size{Cols: 120, Rows: 24}, ac.geometry.Size)
396 require.Equal(t, 120, testAttachmentTab(sess).focusedPane().screen.Columns())396 require.Equal(t, 120, testAttachmentTab(sess).focusedPane().screen.Columns())
397 requireNoOutputFrame(t, sends)397 requireNoOutputFrame(t, sends)
398 }398 }
M internal/usecase/daemon/pane_actions_test.go +3 -3
@@ -660,7 +660,7 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
660660 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}
661661 tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10}
662662 tr, _ := newCapturingTransport(t)
663- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}
663+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
664664 ac.initOverlays()
665665 ac.setSession(sess)
666666 require.True(t, sess.registerAttachment(ac))
@@ -699,7 +699,7 @@ func TestClosePaneRepaintFanoutRespectsAttachmentScope(t *testing.T) {
699699 sends := make([]chan wire.Envelope, 2)
700700 for i := range clients {
701701 tr, sent := newCapturingTransport(t)
702- clients[i] = &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}
702+ clients[i] = &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
703703 clients[i].clientID[0] = byte(i + 1)
704704 clients[i].initOverlays()
705705 clients[i].setSession(sess)
@@ -737,7 +737,7 @@ func TestFinalPaneReapClearsCopyModeForEveryAttachment(t *testing.T) {
737737
738738 for i := range 2 {
739739 tr, _ := newCapturingTransport(t)
740- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}
740+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
741741 ac.clientID[0] = byte(i + 1)
742742 ac.initOverlays()
743743 ac.setSession(sess)
@@ -660,7 +660,7 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) {
660 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}660 tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10}
661 tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10}661 tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10}
662 tr, _ := newCapturingTransport(t)662 tr, _ := newCapturingTransport(t)
663- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}663+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
664 ac.initOverlays()664 ac.initOverlays()
665 ac.setSession(sess)665 ac.setSession(sess)
666 require.True(t, sess.registerAttachment(ac))666 require.True(t, sess.registerAttachment(ac))
@@ -699,7 +699,7 @@ func TestClosePaneRepaintFanoutRespectsAttachmentScope(t *testing.T) {
699 sends := make([]chan wire.Envelope, 2)699 sends := make([]chan wire.Envelope, 2)
700 for i := range clients {700 for i := range clients {
701 tr, sent := newCapturingTransport(t)701 tr, sent := newCapturingTransport(t)
702- clients[i] = &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}702+ clients[i] = &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
703 clients[i].clientID[0] = byte(i + 1)703 clients[i].clientID[0] = byte(i + 1)
704 clients[i].initOverlays()704 clients[i].initOverlays()
705 clients[i].setSession(sess)705 clients[i].setSession(sess)
@@ -737,7 +737,7 @@ func TestFinalPaneReapClearsCopyModeForEveryAttachment(t *testing.T) {
737 737
738 for i := range 2 {738 for i := range 2 {
739 tr, _ := newCapturingTransport(t)739 tr, _ := newCapturingTransport(t)
740- ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 41, Rows: 12}}740+ ac := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 41, Rows: 12}}}
741 ac.clientID[0] = byte(i + 1)741 ac.clientID[0] = byte(i + 1)
742 ac.initOverlays()742 ac.initOverlays()
743 ac.setSession(sess)743 ac.setSession(sess)
M internal/usecase/daemon/performance_benchmark_test.go +5 -5
@@ -265,7 +265,7 @@ func TestPerformanceFixtureResize(t *testing.T) {
265265
266266 for _, want := range sequence {
267267 fixture.resizeTo(want)
268- require.Equal(t, want, fixture.ac.size)
268+ require.Equal(t, want, fixture.ac.geometry.Size)
269269 require.True(t, fixture.resized())
270270 }
271271 metrics := fixture.metrics()
@@ -301,7 +301,7 @@ func TestTransactionalResizeMetrics(t *testing.T) {
301301 require.Equal(t, uint64(3), metrics.outputFrames, "commit, resize-failure notice toast, and successful full-reset retry")
302302 require.Zero(t, metrics.frameGapEpochs, "every committed epoch emitted its required frame")
303303 require.Equal(t, []domain.Size{{Cols: 50, Rows: 28}, {Cols: 50, Rows: 28}}, fixture.pty.requested())
304- require.Equal(t, domain.Size{Cols: 100, Rows: 30}, fixture.ac.size)
304+ require.Equal(t, domain.Size{Cols: 100, Rows: 30}, fixture.ac.geometry.Size)
305305 }
306306
307307 func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
@@ -1077,7 +1077,7 @@ func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, re
10771077 d.clock = clock
10781078 output := &countingOutputTransport{}
10791079 ac.tr = output
1080- ac.size = config.size
1080+ ac.geometry.Size = config.size
10811081 sess.name = "performance"
10821082 sess.ephemeral = false
10831083 sess.incarnation = domain.IncarnationID{1}
@@ -1300,7 +1300,7 @@ func (f *performanceFixture) retryLatest() {
13001300 members := make([]resizeMember, 0)
13011301 for _, tb := range tabs {
13021302 tb.mu.Lock()
1303- plan := prepareTabLayoutForSizeLocked(f.sess, tb, tabSize(f.ac.size))
1303+ plan := prepareTabLayoutForSizeLocked(f.sess, tb, tabSize(f.ac.geometry.Size))
13041304 tb.mu.Unlock()
13051305 members = append(members, plan.members...)
13061306 }
@@ -1314,7 +1314,7 @@ func (f *performanceFixture) retryLatest() {
13141314 }
13151315
13161316 func (f *performanceFixture) resized() bool {
1317- if f.ac.size != f.resizedSize {
1317+ if f.ac.geometry.Size != f.resizedSize {
13181318 return false
13191319 }
13201320 for _, tb := range f.sess.tabs {
@@ -265,7 +265,7 @@ func TestPerformanceFixtureResize(t *testing.T) {
265 265
266 for _, want := range sequence {266 for _, want := range sequence {
267 fixture.resizeTo(want)267 fixture.resizeTo(want)
268- require.Equal(t, want, fixture.ac.size)268+ require.Equal(t, want, fixture.ac.geometry.Size)
269 require.True(t, fixture.resized())269 require.True(t, fixture.resized())
270 }270 }
271 metrics := fixture.metrics()271 metrics := fixture.metrics()
@@ -301,7 +301,7 @@ func TestTransactionalResizeMetrics(t *testing.T) {
301 require.Equal(t, uint64(3), metrics.outputFrames, "commit, resize-failure notice toast, and successful full-reset retry")301 require.Equal(t, uint64(3), metrics.outputFrames, "commit, resize-failure notice toast, and successful full-reset retry")
302 require.Zero(t, metrics.frameGapEpochs, "every committed epoch emitted its required frame")302 require.Zero(t, metrics.frameGapEpochs, "every committed epoch emitted its required frame")
303 require.Equal(t, []domain.Size{{Cols: 50, Rows: 28}, {Cols: 50, Rows: 28}}, fixture.pty.requested())303 require.Equal(t, []domain.Size{{Cols: 50, Rows: 28}, {Cols: 50, Rows: 28}}, fixture.pty.requested())
304- require.Equal(t, domain.Size{Cols: 100, Rows: 30}, fixture.ac.size)304+ require.Equal(t, domain.Size{Cols: 100, Rows: 30}, fixture.ac.geometry.Size)
305 }305 }
306 306
307 func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {307 func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
@@ -1077,7 +1077,7 @@ func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, re
1077 d.clock = clock1077 d.clock = clock
1078 output := &countingOutputTransport{}1078 output := &countingOutputTransport{}
1079 ac.tr = output1079 ac.tr = output
1080- ac.size = config.size1080+ ac.geometry.Size = config.size
1081 sess.name = "performance"1081 sess.name = "performance"
1082 sess.ephemeral = false1082 sess.ephemeral = false
1083 sess.incarnation = domain.IncarnationID{1}1083 sess.incarnation = domain.IncarnationID{1}
@@ -1300,7 +1300,7 @@ func (f *performanceFixture) retryLatest() {
1300 members := make([]resizeMember, 0)1300 members := make([]resizeMember, 0)
1301 for _, tb := range tabs {1301 for _, tb := range tabs {
1302 tb.mu.Lock()1302 tb.mu.Lock()
1303- plan := prepareTabLayoutForSizeLocked(f.sess, tb, tabSize(f.ac.size))1303+ plan := prepareTabLayoutForSizeLocked(f.sess, tb, tabSize(f.ac.geometry.Size))
1304 tb.mu.Unlock()1304 tb.mu.Unlock()
1305 members = append(members, plan.members...)1305 members = append(members, plan.members...)
1306 }1306 }
@@ -1314,7 +1314,7 @@ func (f *performanceFixture) retryLatest() {
1314 }1314 }
1315 1315
1316 func (f *performanceFixture) resized() bool {1316 func (f *performanceFixture) resized() bool {
1317- if f.ac.size != f.resizedSize {1317+ if f.ac.geometry.Size != f.resizedSize {
1318 return false1318 return false
1319 }1319 }
1320 for _, tb := range f.sess.tabs {1320 for _, tb := range f.sess.tabs {
M internal/usecase/daemon/reduced_color_test.go +1 -1
@@ -219,7 +219,7 @@ func TestAppliedThemeIsPerAttachment(t *testing.T) {
219219 d, sess, ac16, _ := newManualSessionWithPTYs(t, p)
220220 ac16.terminalCapabilities = terminalcap.Capabilities{Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}}
221221 tr, _ := newCapturingTransport(t)
222- acTrue := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
222+ acTrue := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
223223 acTrue.output.attachment = acTrue
224224 acTrue.initOverlays()
225225 sess.mu.Lock()
@@ -219,7 +219,7 @@ func TestAppliedThemeIsPerAttachment(t *testing.T) {
219 d, sess, ac16, _ := newManualSessionWithPTYs(t, p)219 d, sess, ac16, _ := newManualSessionWithPTYs(t, p)
220 ac16.terminalCapabilities = terminalcap.Capabilities{Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}}220 ac16.terminalCapabilities = terminalcap.Capabilities{Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}}
221 tr, _ := newCapturingTransport(t)221 tr, _ := newCapturingTransport(t)
222- acTrue := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}222+ acTrue := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
223 acTrue.output.attachment = acTrue223 acTrue.output.attachment = acTrue
224 acTrue.initOverlays()224 acTrue.initOverlays()
225 sess.mu.Lock()225 sess.mu.Lock()
M internal/usecase/daemon/render_coordinator_handshake_test.go +3 -3
@@ -83,9 +83,9 @@ func TestRenderCoordinatorFanoutDoesNotWaitForSlowTransport(t *testing.T) {
8383 d, sess, healthy, healthySends, _ := newManualTabSession(t, 1)
8484 slowTransport := newFanoutBlockingTransport()
8585 slow := &attachedClient{
86- tr: slowTransport,
87- output: newOutputStateStream(),
88- size: domain.Size{Cols: 80, Rows: 24},
86+ tr: slowTransport,
87+ output: newOutputStateStream(),
88+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
8989 }
9090 slow.initOverlays()
9191 slow.setSession(sess)
@@ -83,9 +83,9 @@ func TestRenderCoordinatorFanoutDoesNotWaitForSlowTransport(t *testing.T) {
83 d, sess, healthy, healthySends, _ := newManualTabSession(t, 1)83 d, sess, healthy, healthySends, _ := newManualTabSession(t, 1)
84 slowTransport := newFanoutBlockingTransport()84 slowTransport := newFanoutBlockingTransport()
85 slow := &attachedClient{85 slow := &attachedClient{
86- tr: slowTransport,86+ tr: slowTransport,
87- output: newOutputStateStream(),87+ output: newOutputStateStream(),
88- size: domain.Size{Cols: 80, Rows: 24},88+ geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
89 }89 }
90 slow.initOverlays()90 slow.initOverlays()
91 slow.setSession(sess)91 slow.setSession(sess)
M internal/usecase/daemon/render_test.go +2 -2
@@ -73,7 +73,7 @@ func TestFirstPaintRetainedFloatingPaneEmitsOneReset(t *testing.T) {
7373 defer releasePTY()
7474 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
7575 ac.sendMu.Lock()
76- ac.size = tc.clientSize
76+ ac.geometry.Size = tc.clientSize
7777 ac.sendMu.Unlock()
7878
7979 // A retained visible popup still needs activation warmup. When the
@@ -404,7 +404,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t
404404 rc := d.attachCoordinator(target, nil, detached, true)
405405
406406 secondTransport, secondSends := newCapturingTransport(t)
407- remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), size: detached.size}
407+ remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: detached.geometry.Size}}
408408 remaining.initOverlays()
409409 remaining.setSession(target)
410410 target.mu.Lock()
@@ -73,7 +73,7 @@ func TestFirstPaintRetainedFloatingPaneEmitsOneReset(t *testing.T) {
73 defer releasePTY()73 defer releasePTY()
74 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)74 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
75 ac.sendMu.Lock()75 ac.sendMu.Lock()
76- ac.size = tc.clientSize76+ ac.geometry.Size = tc.clientSize
77 ac.sendMu.Unlock()77 ac.sendMu.Unlock()
78 78
79 // A retained visible popup still needs activation warmup. When the79 // A retained visible popup still needs activation warmup. When the
@@ -404,7 +404,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t
404 rc := d.attachCoordinator(target, nil, detached, true)404 rc := d.attachCoordinator(target, nil, detached, true)
405 405
406 secondTransport, secondSends := newCapturingTransport(t)406 secondTransport, secondSends := newCapturingTransport(t)
407- remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), size: detached.size}407+ remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), geometry: domain.Geometry{Size: detached.geometry.Size}}
408 remaining.initOverlays()408 remaining.initOverlays()
409 remaining.setSession(target)409 remaining.setSession(target)
410 target.mu.Lock()410 target.mu.Lock()
M internal/usecase/daemon/resume_test.go +1 -1
@@ -1247,7 +1247,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
12471247 require.Len(t, sends, 1)
12481248 output := unmarshalTestOutput(t, sends[0].Payload)
12491249 require.Zero(t, output.Base, "resume must start with a complete frame")
1250- terminal := vt.NewScreen(resumedAC.size.Cols, resumedAC.size.Rows)
1250+ terminal := vt.NewScreen(resumedAC.geometry.Size.Cols, resumedAC.geometry.Size.Rows)
12511251 terminal.Write(output.Data)
12521252 contents := strings.Join(frameRows(terminal), "\n")
12531253 require.Contains(t, contents, "survivor", "resume first paint must contain current headless content")
@@ -1247,7 +1247,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
1247 require.Len(t, sends, 1)1247 require.Len(t, sends, 1)
1248 output := unmarshalTestOutput(t, sends[0].Payload)1248 output := unmarshalTestOutput(t, sends[0].Payload)
1249 require.Zero(t, output.Base, "resume must start with a complete frame")1249 require.Zero(t, output.Base, "resume must start with a complete frame")
1250- terminal := vt.NewScreen(resumedAC.size.Cols, resumedAC.size.Rows)1250+ terminal := vt.NewScreen(resumedAC.geometry.Size.Cols, resumedAC.geometry.Size.Rows)
1251 terminal.Write(output.Data)1251 terminal.Write(output.Data)
1252 contents := strings.Join(frameRows(terminal), "\n")1252 contents := strings.Join(frameRows(terminal), "\n")
1253 require.Contains(t, contents, "survivor", "resume first paint must contain current headless content")1253 require.Contains(t, contents, "survivor", "resume first paint must contain current headless content")
M internal/usecase/daemon/session.go +0 -6
@@ -2169,12 +2169,6 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
21692169 markSnapshotDirty(sess)
21702170 }
21712171
2172-// childEnv retains the daemon-environment helper for daemon-local legacy callers.
2173-// Interactive PTY launch paths use childEnvFrom with their session snapshot.
2174-func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string {
2175- return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID)
2176-}
2177-
21782172 func copyEnvironment(env []string) []string {
21792173 return append([]string(nil), env...)
21802174 }
@@ -2169,12 +2169,6 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
2169 markSnapshotDirty(sess)2169 markSnapshotDirty(sess)
2170 }2170 }
2171 2171
2172-// childEnv retains the daemon-environment helper for daemon-local legacy callers.
2173-// Interactive PTY launch paths use childEnvFrom with their session snapshot.
2174-func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string {
2175- return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID)
2176-}
2177-
2178 func copyEnvironment(env []string) []string {2172 func copyEnvironment(env []string) []string {
2179 return append([]string(nil), env...)2173 return append([]string(nil), env...)
2180 }2174 }
M internal/usecase/daemon/session_test.go +7 -7
@@ -1255,7 +1255,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
12551255 d.ptys = newFactorySeq(t, firstPTY, secondPTY)
12561256 WithStore(t, store)(d)
12571257
1258- first, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)
1258+ first, err := createSessionForTest(d, "0", true, "/tmp", ac.geometry.Size, terminalEnv{}, d.baseEnv)
12591259 require.NoError(t, err)
12601260 require.NoError(t, d.renameSession(first, "named"))
12611261 staleCreatedAt := first.createdAt
@@ -1263,7 +1263,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
12631263 require.NoError(t, d.killSession(first, protocol.ReasonSessionKilled, true))
12641264 d.sessWg.Wait()
12651265
1266- second, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)
1266+ second, err := createSessionForTest(d, "0", true, "/tmp", ac.geometry.Size, terminalEnv{}, d.baseEnv)
12671267 require.NoError(t, err)
12681268 require.NoError(t, d.renameSession(second, "named"))
12691269 require.NotEqual(t, staleCreatedAt, second.createdAt)
@@ -1414,7 +1414,7 @@ func TestPickerStoppedTargetKillPurges(t *testing.T) {
14141414 func TestChildEnvEscapesLegacySessionName(t *testing.T) {
14151415 d := newTestDaemon(t, nil, stubClock{})
14161416
1417- got := d.childEnv("legacy,name=value", "t_alpha", "p_beta")
1417+ got := childEnvFrom(d.baseEnv, "legacy,name=value", "t_alpha", "p_beta")
14181418
14191419 require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta")
14201420 }
@@ -1580,7 +1580,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) {
15801580 d := newTestDaemon(t, nil, stubClock{})
15811581 d.baseEnv = tt.baseEnv
15821582
1583- got := d.childEnv("work", "t_alpha", "p_beta")
1583+ got := childEnvFrom(d.baseEnv, "work", "t_alpha", "p_beta")
15841584
15851585 for _, want := range tt.wantContain {
15861586 require.Contains(t, got, want)
@@ -1629,7 +1629,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
16291629 require.Contains(t, opens[0], "TERM=xterm-256color")
16301630 require.Contains(t, opens[0], "COLORTERM=truecolor")
16311631 require.Contains(t, opens[0], "TERM_PROGRAM=vev")
1632- require.NoError(t, d.createTab(sess, ac.size))
1632+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
16331633 require.Contains(t, opens[1], "TERM=xterm-256color")
16341634 require.Contains(t, opens[1], "COLORTERM=truecolor")
16351635 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
@@ -1675,7 +1675,7 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
16751675
16761676 require.Contains(t, opens[0], "TERM=xterm-256color")
16771677 require.Contains(t, opens[0], "COLORTERM=truecolor")
1678- require.NoError(t, d.createTab(sess, ac.size))
1678+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
16791679 require.Contains(t, opens[1], "TERM=xterm-256color")
16801680 require.Contains(t, opens[1], "COLORTERM=truecolor")
16811681 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
@@ -1792,7 +1792,7 @@ func TestAttachEnvironmentRefreshesOnlySessionBoundFuturePTYInputs(t *testing.T)
17921792 require.Equal(t, "/usr/bin/fish", commands[0])
17931793 require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "WAYLAND_DISPLAY=wayland-1", "SSH_AUTH_SOCK=/run/first-agent", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0])
17941794 require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator")
1795- require.NoError(t, d.createTab(sess, ac.size))
1795+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
17961796 // Only session-bound variables follow the attaching client: the X11-only
17971797 // display group replaces the Wayland one, the absent agent socket is
17981798 // removed, and the creator's shell and ordinary variables stay.
@@ -1255,7 +1255,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
1255 d.ptys = newFactorySeq(t, firstPTY, secondPTY)1255 d.ptys = newFactorySeq(t, firstPTY, secondPTY)
1256 WithStore(t, store)(d)1256 WithStore(t, store)(d)
1257 1257
1258- first, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)1258+ first, err := createSessionForTest(d, "0", true, "/tmp", ac.geometry.Size, terminalEnv{}, d.baseEnv)
1259 require.NoError(t, err)1259 require.NoError(t, err)
1260 require.NoError(t, d.renameSession(first, "named"))1260 require.NoError(t, d.renameSession(first, "named"))
1261 staleCreatedAt := first.createdAt1261 staleCreatedAt := first.createdAt
@@ -1263,7 +1263,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
1263 require.NoError(t, d.killSession(first, protocol.ReasonSessionKilled, true))1263 require.NoError(t, d.killSession(first, protocol.ReasonSessionKilled, true))
1264 d.sessWg.Wait()1264 d.sessWg.Wait()
1265 1265
1266- second, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)1266+ second, err := createSessionForTest(d, "0", true, "/tmp", ac.geometry.Size, terminalEnv{}, d.baseEnv)
1267 require.NoError(t, err)1267 require.NoError(t, err)
1268 require.NoError(t, d.renameSession(second, "named"))1268 require.NoError(t, d.renameSession(second, "named"))
1269 require.NotEqual(t, staleCreatedAt, second.createdAt)1269 require.NotEqual(t, staleCreatedAt, second.createdAt)
@@ -1414,7 +1414,7 @@ func TestPickerStoppedTargetKillPurges(t *testing.T) {
1414 func TestChildEnvEscapesLegacySessionName(t *testing.T) {1414 func TestChildEnvEscapesLegacySessionName(t *testing.T) {
1415 d := newTestDaemon(t, nil, stubClock{})1415 d := newTestDaemon(t, nil, stubClock{})
1416 1416
1417- got := d.childEnv("legacy,name=value", "t_alpha", "p_beta")1417+ got := childEnvFrom(d.baseEnv, "legacy,name=value", "t_alpha", "p_beta")
1418 1418
1419 require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta")1419 require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta")
1420 }1420 }
@@ -1580,7 +1580,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) {
1580 d := newTestDaemon(t, nil, stubClock{})1580 d := newTestDaemon(t, nil, stubClock{})
1581 d.baseEnv = tt.baseEnv1581 d.baseEnv = tt.baseEnv
1582 1582
1583- got := d.childEnv("work", "t_alpha", "p_beta")1583+ got := childEnvFrom(d.baseEnv, "work", "t_alpha", "p_beta")
1584 1584
1585 for _, want := range tt.wantContain {1585 for _, want := range tt.wantContain {
1586 require.Contains(t, got, want)1586 require.Contains(t, got, want)
@@ -1629,7 +1629,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1629 require.Contains(t, opens[0], "TERM=xterm-256color")1629 require.Contains(t, opens[0], "TERM=xterm-256color")
1630 require.Contains(t, opens[0], "COLORTERM=truecolor")1630 require.Contains(t, opens[0], "COLORTERM=truecolor")
1631 require.Contains(t, opens[0], "TERM_PROGRAM=vev")1631 require.Contains(t, opens[0], "TERM_PROGRAM=vev")
1632- require.NoError(t, d.createTab(sess, ac.size))1632+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
1633 require.Contains(t, opens[1], "TERM=xterm-256color")1633 require.Contains(t, opens[1], "TERM=xterm-256color")
1634 require.Contains(t, opens[1], "COLORTERM=truecolor")1634 require.Contains(t, opens[1], "COLORTERM=truecolor")
1635 require.Contains(t, opens[1], "TERM_PROGRAM=vev")1635 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
@@ -1675,7 +1675,7 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1675 1675
1676 require.Contains(t, opens[0], "TERM=xterm-256color")1676 require.Contains(t, opens[0], "TERM=xterm-256color")
1677 require.Contains(t, opens[0], "COLORTERM=truecolor")1677 require.Contains(t, opens[0], "COLORTERM=truecolor")
1678- require.NoError(t, d.createTab(sess, ac.size))1678+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
1679 require.Contains(t, opens[1], "TERM=xterm-256color")1679 require.Contains(t, opens[1], "TERM=xterm-256color")
1680 require.Contains(t, opens[1], "COLORTERM=truecolor")1680 require.Contains(t, opens[1], "COLORTERM=truecolor")
1681 require.Contains(t, opens[1], "TERM_PROGRAM=vev")1681 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
@@ -1792,7 +1792,7 @@ func TestAttachEnvironmentRefreshesOnlySessionBoundFuturePTYInputs(t *testing.T)
1792 require.Equal(t, "/usr/bin/fish", commands[0])1792 require.Equal(t, "/usr/bin/fish", commands[0])
1793 require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "WAYLAND_DISPLAY=wayland-1", "SSH_AUTH_SOCK=/run/first-agent", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0])1793 require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "WAYLAND_DISPLAY=wayland-1", "SSH_AUTH_SOCK=/run/first-agent", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0])
1794 require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator")1794 require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator")
1795- require.NoError(t, d.createTab(sess, ac.size))1795+ require.NoError(t, d.createTab(sess, ac.geometry.Size))
1796 // Only session-bound variables follow the attaching client: the X11-only1796 // Only session-bound variables follow the attaching client: the X11-only
1797 // display group replaces the Wayland one, the absent agent socket is1797 // display group replaces the Wayland one, the absent agent socket is
1798 // removed, and the creator's shell and ordinary variables stay.1798 // removed, and the creator's shell and ordinary variables stay.
M internal/usecase/daemon/transactional_resize_test.go +2 -2
@@ -474,7 +474,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) {
474474 lease := rc.attachmentLease(ac)
475475 epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease)
476476 require.NotZero(t, epoch)
477- initialSize := ac.size
477+ initialSize := ac.geometry.Size
478478
479479 entered := make(chan struct{})
480480 release := make(chan struct{})
@@ -492,7 +492,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) {
492492 require.NotZero(t, newer)
493493 close(release)
494494 require.False(t, <-done, "stale resize generation must be rejected")
495- require.Equal(t, initialSize, ac.size, "stale resize must not publish attachment size")
495+ require.Equal(t, initialSize, ac.geometry.Size, "stale resize must not publish attachment size")
496496 if tc.effect == resizeOwnerPostSnapshotDirty {
497497 require.False(t, sess.snapDirty.Load(), "stale resize must not publish snapshot dirtiness")
498498 }
@@ -474,7 +474,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) {
474 lease := rc.attachmentLease(ac)474 lease := rc.attachmentLease(ac)
475 epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease)475 epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease)
476 require.NotZero(t, epoch)476 require.NotZero(t, epoch)
477- initialSize := ac.size477+ initialSize := ac.geometry.Size
478 478
479 entered := make(chan struct{})479 entered := make(chan struct{})
480 release := make(chan struct{})480 release := make(chan struct{})
@@ -492,7 +492,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) {
492 require.NotZero(t, newer)492 require.NotZero(t, newer)
493 close(release)493 close(release)
494 require.False(t, <-done, "stale resize generation must be rejected")494 require.False(t, <-done, "stale resize generation must be rejected")
495- require.Equal(t, initialSize, ac.size, "stale resize must not publish attachment size")495+ require.Equal(t, initialSize, ac.geometry.Size, "stale resize must not publish attachment size")
496 if tc.effect == resizeOwnerPostSnapshotDirty {496 if tc.effect == resizeOwnerPostSnapshotDirty {
497 require.False(t, sess.snapDirty.Load(), "stale resize must not publish snapshot dirtiness")497 require.False(t, sess.snapDirty.Load(), "stale resize must not publish snapshot dirtiness")
498 }498 }

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