detached 6b7d2c3

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 (
99 "github.com/stretchr/testify/require"
1010 )
1111
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+
1289 func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) {
1390 d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY())
1491 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] = 191 first.clientID[0] = 1
M internal/usecase/daemon/palette.go +1 -1
@@ -435,7 +435,7 @@ func (e paletteExec) runAction(request daemonActionRequest) error {
435435 }
436436
437437 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()})
439439 }
440440
441441 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 {
513513 return d.createTabForAttachment(sess, nil, sz)
514514 }
515515
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 {
517517 sess.mu.Lock()
518518 name := sess.name
519519 cwd := sess.cwd
@@ -521,7 +521,7 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, sz do
521521 env := copyEnvironment(sess.env)
522522 attachments := sess.snapshotAttachmentsLocked()
523523 sess.mu.Unlock()
524- tbSize := contentSize(sz)
524+ tbSize := contentSize(sess.fullViewportSize())
525525 tabStableID, paneStableID, err := d.newTabPaneStableIDs()
526526 if err != nil {
527527 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.name518 name := sess.name
519 cwd := sess.cwd519 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 err527 return err

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