fix(daemon): use session geometry for new tabs
- commit
- 6b7d2c30776393d4e67b771fa01e8c7e4bf5c35e
- author
- brice · · 2026-08-03 15:29 UTC
- committer
- brice · · 2026-08-04 13:34 UTC
- parent
- 3fedffb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +80 -3
M internal/usecase/daemon/final_acceptance_test.go +77 -0
| @@ -9,6 +9,83 @@ import ( | |||
| 9 | 9 | "github.com/stretchr/testify/require" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *testing.T) { | |
| 13 | + | d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 14 | + | first.clientID[0] = 1 | |
| 15 | + | firstTab := sess.tabs[0] | |
| 16 | + | firstTab.stableID = "tab-first" | |
| 17 | + | firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first" | |
| 18 | + | ||
| 19 | + | secondTransport, _ := newCapturingTransport(t) | |
| 20 | + | second := &attachedClient{ | |
| 21 | + | tr: secondTransport, | |
| 22 | + | output: newOutputStateStream(), | |
| 23 | + | size: domain.Size{Cols: 80, Rows: 24}, | |
| 24 | + | } | |
| 25 | + | second.initOverlays() | |
| 26 | + | second.clientID[0] = 2 | |
| 27 | + | second.setSession(sess) | |
| 28 | + | secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23}) | |
| 29 | + | secondTab.ctx, secondTab.cancel = sess.ctx, func() {} | |
| 30 | + | publishTiledPaneOwners(sess, secondTab) | |
| 31 | + | require.NoError(t, sess.runMutation(func() error { | |
| 32 | + | sess.mu.Lock() | |
| 33 | + | defer sess.mu.Unlock() | |
| 34 | + | sess.tabs = append(sess.tabs, secondTab) | |
| 35 | + | return nil | |
| 36 | + | })) | |
| 37 | + | require.True(t, sess.registerAttachment(second)) | |
| 38 | + | require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID))) | |
| 39 | + | require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID))) | |
| 40 | + | ||
| 41 | + | d.ptys = newFactory(t, newQuietPTY()) | |
| 42 | + | rc := d.attachCoordinator(sess, nil, first, true) | |
| 43 | + | d.attachCoordinator(sess, nil, second, true) | |
| 44 | + | firstToken := sess.attachmentToken(first, first.transport()) | |
| 45 | + | firstToken.lease = rc.attachmentLease(first) | |
| 46 | + | first.publishAttachmentCapability(firstToken) | |
| 47 | + | secondToken := sess.attachmentToken(second, second.transport()) | |
| 48 | + | secondToken.lease = rc.attachmentLease(second) | |
| 49 | + | second.publishAttachmentCapability(secondToken) | |
| 50 | + | ||
| 51 | + | firstView := first.viewSnapshot() | |
| 52 | + | secondView := second.viewSnapshot() | |
| 53 | + | first.sendMu.Lock() | |
| 54 | + | firstEpoch := first.output.currentEpoch() | |
| 55 | + | first.sendMu.Unlock() | |
| 56 | + | require.True(t, d.resizeAttachmentForLease(secondToken, domain.Size{Cols: 120, Rows: 40})) | |
| 57 | + | ||
| 58 | + | d.enterPalette(sess, second) | |
| 59 | + | d.handlePaletteInput(second, []byte("CNT\r")) | |
| 60 | + | ||
| 61 | + | sess.mu.Lock() | |
| 62 | + | tabs := append([]*tab(nil), sess.tabs...) | |
| 63 | + | sess.mu.Unlock() | |
| 64 | + | require.Len(t, tabs, 3) | |
| 65 | + | wantContent := domain.Size{Cols: 80, Rows: 23} | |
| 66 | + | for _, tb := range tabs { | |
| 67 | + | tb.mu.Lock() | |
| 68 | + | gotTabSize := tb.size | |
| 69 | + | pane := tb.focusedPane() | |
| 70 | + | pane.mu.Lock() | |
| 71 | + | gotPaneSize := domain.Size{Cols: pane.screen.Frame.Width, Rows: pane.screen.Frame.Height} | |
| 72 | + | pane.mu.Unlock() | |
| 73 | + | tb.mu.Unlock() | |
| 74 | + | require.Equal(t, wantContent, gotTabSize) | |
| 75 | + | require.Equal(t, wantContent, gotPaneSize) | |
| 76 | + | } | |
| 77 | + | require.Equal(t, firstView.tabID, first.viewSnapshot().tabID, "new tab selection crossed attachment boundary") | |
| 78 | + | require.NotEqual(t, secondView.tabID, second.viewSnapshot().tabID, "palette did not select the created tab") | |
| 79 | + | first.sendMu.Lock() | |
| 80 | + | gotFirstEpoch := first.output.currentEpoch() | |
| 81 | + | first.sendMu.Unlock() | |
| 82 | + | require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary") | |
| 83 | + | second.sendMu.Lock() | |
| 84 | + | gotSecondSize := second.size | |
| 85 | + | second.sendMu.Unlock() | |
| 86 | + | require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize) | |
| 87 | + | } | |
| 88 | + | ||
| 12 | 89 | func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) { | |
| 13 | 90 | d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 14 | 91 | first.clientID[0] = 1 | |
| @@ -9,6 +9,83 @@ import ( | |||||
| 9 | "github.com/stretchr/testify/require" | 9 | "github.com/stretchr/testify/require" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *testing.T) { | |||
| 13 | + | d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |||
| 14 | + | first.clientID[0] = 1 | |||
| 15 | + | firstTab := sess.tabs[0] | |||
| 16 | + | firstTab.stableID = "tab-first" | |||
| 17 | + | firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first" | |||
| 18 | + | ||||
| 19 | + | secondTransport, _ := newCapturingTransport(t) | |||
| 20 | + | second := &attachedClient{ | |||
| 21 | + | tr: secondTransport, | |||
| 22 | + | output: newOutputStateStream(), | |||
| 23 | + | size: domain.Size{Cols: 80, Rows: 24}, | |||
| 24 | + | } | |||
| 25 | + | second.initOverlays() | |||
| 26 | + | second.clientID[0] = 2 | |||
| 27 | + | second.setSession(sess) | |||
| 28 | + | secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23}) | |||
| 29 | + | secondTab.ctx, secondTab.cancel = sess.ctx, func() {} | |||
| 30 | + | publishTiledPaneOwners(sess, secondTab) | |||
| 31 | + | require.NoError(t, sess.runMutation(func() error { | |||
| 32 | + | sess.mu.Lock() | |||
| 33 | + | defer sess.mu.Unlock() | |||
| 34 | + | sess.tabs = append(sess.tabs, secondTab) | |||
| 35 | + | return nil | |||
| 36 | + | })) | |||
| 37 | + | require.True(t, sess.registerAttachment(second)) | |||
| 38 | + | require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID))) | |||
| 39 | + | require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID))) | |||
| 40 | + | ||||
| 41 | + | d.ptys = newFactory(t, newQuietPTY()) | |||
| 42 | + | rc := d.attachCoordinator(sess, nil, first, true) | |||
| 43 | + | d.attachCoordinator(sess, nil, second, true) | |||
| 44 | + | firstToken := sess.attachmentToken(first, first.transport()) | |||
| 45 | + | firstToken.lease = rc.attachmentLease(first) | |||
| 46 | + | first.publishAttachmentCapability(firstToken) | |||
| 47 | + | secondToken := sess.attachmentToken(second, second.transport()) | |||
| 48 | + | secondToken.lease = rc.attachmentLease(second) | |||
| 49 | + | second.publishAttachmentCapability(secondToken) | |||
| 50 | + | ||||
| 51 | + | firstView := first.viewSnapshot() | |||
| 52 | + | secondView := second.viewSnapshot() | |||
| 53 | + | first.sendMu.Lock() | |||
| 54 | + | firstEpoch := first.output.currentEpoch() | |||
| 55 | + | first.sendMu.Unlock() | |||
| 56 | + | require.True(t, d.resizeAttachmentForLease(secondToken, domain.Size{Cols: 120, Rows: 40})) | |||
| 57 | + | ||||
| 58 | + | d.enterPalette(sess, second) | |||
| 59 | + | d.handlePaletteInput(second, []byte("CNT\r")) | |||
| 60 | + | ||||
| 61 | + | sess.mu.Lock() | |||
| 62 | + | tabs := append([]*tab(nil), sess.tabs...) | |||
| 63 | + | sess.mu.Unlock() | |||
| 64 | + | require.Len(t, tabs, 3) | |||
| 65 | + | wantContent := domain.Size{Cols: 80, Rows: 23} | |||
| 66 | + | for _, tb := range tabs { | |||
| 67 | + | tb.mu.Lock() | |||
| 68 | + | gotTabSize := tb.size | |||
| 69 | + | pane := tb.focusedPane() | |||
| 70 | + | pane.mu.Lock() | |||
| 71 | + | gotPaneSize := domain.Size{Cols: pane.screen.Frame.Width, Rows: pane.screen.Frame.Height} | |||
| 72 | + | pane.mu.Unlock() | |||
| 73 | + | tb.mu.Unlock() | |||
| 74 | + | require.Equal(t, wantContent, gotTabSize) | |||
| 75 | + | require.Equal(t, wantContent, gotPaneSize) | |||
| 76 | + | } | |||
| 77 | + | require.Equal(t, firstView.tabID, first.viewSnapshot().tabID, "new tab selection crossed attachment boundary") | |||
| 78 | + | require.NotEqual(t, secondView.tabID, second.viewSnapshot().tabID, "palette did not select the created tab") | |||
| 79 | + | first.sendMu.Lock() | |||
| 80 | + | gotFirstEpoch := first.output.currentEpoch() | |||
| 81 | + | first.sendMu.Unlock() | |||
| 82 | + | require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary") | |||
| 83 | + | second.sendMu.Lock() | |||
| 84 | + | gotSecondSize := second.size | |||
| 85 | + | second.sendMu.Unlock() | |||
| 86 | + | require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 12 | func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) { | 89 | func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) { | ||
| 13 | d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 90 | d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 14 | first.clientID[0] = 1 | 91 | first.clientID[0] = 1 | ||
M internal/usecase/daemon/palette.go +1 -1
| @@ -435,7 +435,7 @@ func (e paletteExec) runAction(request daemonActionRequest) error { | |||
| 435 | 435 | } | |
| 436 | 436 | ||
| 437 | 437 | func (e paletteExec) CreateTab() error { | |
| 438 | - | return e.runAction(daemonActionRequest{kind: daemonActionCreateTab, viewport: e.ac.size}) | |
| 438 | + | return e.runAction(daemonActionRequest{kind: daemonActionCreateTab, viewport: e.sess.fullViewportSize()}) | |
| 439 | 439 | } | |
| 440 | 440 | ||
| 441 | 441 | func (e paletteExec) CreateSession() error { | |
| @@ -435,7 +435,7 @@ func (e paletteExec) runAction(request daemonActionRequest) error { | |||||
| 435 | } | 435 | } | ||
| 436 | 436 | ||||
| 437 | func (e paletteExec) CreateTab() error { | 437 | func (e paletteExec) CreateTab() error { | ||
| 438 | - | return e.runAction(daemonActionRequest{kind: daemonActionCreateTab, viewport: e.ac.size}) | 438 | + | return e.runAction(daemonActionRequest{kind: daemonActionCreateTab, viewport: e.sess.fullViewportSize()}) |
| 439 | } | 439 | } | ||
| 440 | 440 | ||||
| 441 | func (e paletteExec) CreateSession() error { | 441 | func (e paletteExec) CreateSession() error { | ||
M internal/usecase/daemon/session.go +2 -2
| @@ -513,7 +513,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 513 | 513 | return d.createTabForAttachment(sess, nil, sz) | |
| 514 | 514 | } | |
| 515 | 515 | ||
| 516 | - | func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, sz domain.Size) error { | |
| 516 | + | func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ domain.Size) error { | |
| 517 | 517 | sess.mu.Lock() | |
| 518 | 518 | name := sess.name | |
| 519 | 519 | cwd := sess.cwd | |
| @@ -521,7 +521,7 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, sz do | |||
| 521 | 521 | env := copyEnvironment(sess.env) | |
| 522 | 522 | attachments := sess.snapshotAttachmentsLocked() | |
| 523 | 523 | sess.mu.Unlock() | |
| 524 | - | tbSize := contentSize(sz) | |
| 524 | + | tbSize := contentSize(sess.fullViewportSize()) | |
| 525 | 525 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | |
| 526 | 526 | if err != nil { | |
| 527 | 527 | return err | |
| @@ -513,7 +513,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 513 | return d.createTabForAttachment(sess, nil, sz) | 513 | return d.createTabForAttachment(sess, nil, sz) | ||
| 514 | } | 514 | } | ||
| 515 | 515 | ||||
| 516 | - | func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, sz domain.Size) error { | 516 | + | func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ domain.Size) error { |
| 517 | sess.mu.Lock() | 517 | sess.mu.Lock() | ||
| 518 | name := sess.name | 518 | name := sess.name | ||
| 519 | cwd := sess.cwd | 519 | cwd := sess.cwd | ||
| @@ -521,7 +521,7 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, sz do | |||||
| 521 | env := copyEnvironment(sess.env) | 521 | env := copyEnvironment(sess.env) | ||
| 522 | attachments := sess.snapshotAttachmentsLocked() | 522 | attachments := sess.snapshotAttachmentsLocked() | ||
| 523 | sess.mu.Unlock() | 523 | sess.mu.Unlock() | ||
| 524 | - | tbSize := contentSize(sz) | 524 | + | tbSize := contentSize(sess.fullViewportSize()) |
| 525 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | 525 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | ||
| 526 | if err != nil { | 526 | if err != nil { | ||
| 527 | return err | 527 | return err | ||