Skip to content

Keep hidden tmux clients out of window sizing - #228

Merged
wesm merged 28 commits into
mainfrom
fix/hidden-tmux-client-sizing
Sep 3, 2026
Merged

Keep hidden tmux clients out of window sizing#228
wesm merged 28 commits into
mainfrom
fix/hidden-tmux-client-sizing

Conversation

@wesm

@wesm wesm commented Sep 1, 2026

Copy link
Copy Markdown
Member

Hidden retained tmux clients no longer resize the shared session viewport. Before this change, navigating away from a tmux session left its retained client attached as an ordinary sizing client, so the session kept following that invisible client's grid instead of the one the user was looking at.

  • Every hidden retained client on a POSIX host carries tmux's client-local ignore-size flag, whether it started hidden, was hidden after use, or reconnected. Ghosthub clears the flag on the exact client before making its surface interactive, and never touches the session's global window-size policy.
  • Hide and reopen transitions are serialized per attachment. A transition cancelled by navigation cannot run later, and a failed exact-client mutation detaches the retained client rather than leaving an invisible sizing client behind.
  • Activating a session whose host cannot be resolved still hides the client the user left.
  • tmux older than 3.4 has no safe exact-client targeting. Those clients stay usable while active but are detached instead of retained when hidden.

Behaviour changes beyond sizing:

  • Windows/psmux has no non-sizing client mode. Inactive Windows clients are no longer restored after a host reconnect or a failed worktree removal; they wait for an explicit open. Active clients are unaffected.
  • Inactive retained clients that need kwt to establish their workspace are no longer restored automatically for the same reason; the active one still is. The sidebar shows them as not running until opened.
  • Switching between retained sessions now waits for one refresh-client round trip before the new client is committed active. On remote hosts that is one SSH command per switch.

wesm and others added 4 commits September 1, 2026 13:36
A hidden client can race an immediate reopen while tmux updates its client flags. Main actor isolation does not preserve command order across those suspended operations.

Queue each attachment's sizing changes and retain the latest intent during provisioning. An older hide can no longer leave a reopened client in non-sizing mode.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
An ordinary tmux client stayed attached as a sizing authority after its presentation was hidden. That invisible client could shrink the shared session viewport even when previews were off.

Make hidden POSIX clients non-sizing and restore interactive sizing before reuse. If Ghosthub cannot update the exact client safely, detach it instead of retaining an invisible sizing client.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
A reconnect could restore sizing authority to an ordinary hidden tmux client.
That invisible client could then resize the shared session before it was shown
again.

Carry the retained presentation's sizing intent into every reconnect. Windows
clients remain user-owned, and Ghosthub does not change the global window-size
policy.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
A canceled scene operation could leave its queued sizing transition running.
That obsolete work could issue a tmux command after a newer presentation state
had taken over.

Propagate caller cancellation into the serialized transition and stop before
the tmux operation starts. This keeps the queue ordered without reviving work
that its owner canceled.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 1, 2026 19:57 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 1, 2026 19:57 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 1, 2026 19:57 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (2e33cb8)

Verdict: One Medium-severity issue identified; otherwise, no issues were reported.

Medium

  • Sources/App/WorkspaceSceneModel.swift:10354-10373 — Reopening a session while a sizing transition is pending preserves the old navigation revision. The transition fails its revision guard, and stale cleanup repeatedly re-drives activation, causing an endless task loop that prevents the session from becoming active.
    • Fix: Replace the pending revision with the current navigation revision for each new activation, and ensure stale cleanup clears or avoids re-driving obsolete pending activations.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 6m47s

A second reopen could leave a retained tmux presentation tied to an older
navigation revision. Its sizing task then rejected its own activation and
repeatedly restarted without publishing the session.

Make each reopen replace the pending revision and retire obsolete cleanup.
Give the affected remote tests an exact-client response so they exercise the
successful hidden-sizing path instead of racing an intentional detach.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 1, 2026 23:59 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 1, 2026 23:59 — with GitHub Actions Active
@wesm

wesm commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Fixed in 7405062.

  • Each reopen now replaces the pending sizing activation revision with the current navigation revision.
  • Obsolete transition cleanup clears stale intent instead of re-driving activation.
  • The repeated-reopen regression now completes and publishes the interactive client.
  • The two hosted CI failures now use an exact-client identity fixture for the hidden-sizing transition.

make swift-test, make test-essential-workflows, make build, and make format pass locally.

@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 00:00 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (7405062)

Verdict: One medium-severity lifecycle issue was identified; otherwise, no security vulnerabilities were found.

  • MediumSources/App/WorkspaceSceneModel.swift:10366-10377
    During attachment provisioning, activation returns .pending, but cleanup clears pendingSizingActivationNavigationRevision. After provisioning, the staged active handle is never committed, leaving the reopened session selected but disconnected until opened again. Preserve the pending activation marker and re-drive activation after attachment readiness.

Reviewers: 2 done | Synthesis: codex, 5s | Total: 9m11s

Reopening a hidden session while its attachment was still resolving could
leave it selected without an active client. The sizing transition correctly
deferred to provisioning, but its scene marker disappeared before readiness
could resume it.

Keep current activation intent until surface readiness and discard only
obsolete intent. This lets the selected client become interactive without
reviving stale navigation.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 01:17 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 01:17 — with GitHub Actions Active
@wesm

wesm commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Fixed in 86002b4.

  • A current pending activation now keeps its navigation marker while the attachment is provisioning.
  • Attachment readiness resumes interactive sizing and commits the staged active handle.
  • Obsolete activation markers are still cleared, so stale navigation cannot restart activation.
  • The regression test blocks attachment resolution, hides and reopens the session, then verifies that readiness publishes a connected active session.

make swift-test, make test-essential-workflows, make build, and make format pass locally. The pre-commit warnings-as-errors build also passes.

@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 01:17 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (86002b4)

Verdict: Three medium-severity tmux sizing lifecycle issues remain.

Medium

  • Sources/App/WorkspaceSceneModel.swift:10555-10561; Sources/App/NativeTmuxSessionCoordinator.swift:769-770 — Hiding an interactive client applies the preview grid before enabling tmux’s ignore-size, allowing the local resize to affect the shared tmux window. Set ignore-size first, then apply the preview grid.

  • Sources/App/NativeTmuxSessionCoordinator.swift:556-557, 1485-1486 — Removing the sizing tail does not cancel the underlying transition task, so in-flight sizing commands may continue after detachment or shutdown. Retain and cancel the actual transition task, draining it before releasing attachment resources.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 16m32s

A hidden client could apply its preview grid before tmux stopped using that
client for shared window sizing. Detach and shutdown could also release an
attachment while its last sizing command was still running.

Set ignore-size before changing local preview geometry. Keep ownership of
every sizing task through teardown, cancel it, and wait before releasing the
SSH attachment.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 02:48 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 02:48 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 02:49 — with GitHub Actions Active
@wesm

wesm commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Fixed in 55da47a.

  • tmux now receives ignore-size before Ghosthub applies a hidden preview grid, so the local resize cannot change the shared window.
  • Detach and shutdown cancel and drain the actual serialized sizing tasks before releasing the SSH attachment.
  • Regression coverage observes the real tmux client flag during grid application and verifies cancellation before release for both teardown paths.

make swift-test (1,969 tests), make test-essential-workflows, make build, and make format pass locally. The pre-commit warnings-as-errors build also passes.

@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (55da47a)

Verdict: Two medium-severity issues remain; otherwise, no additional concerns were identified.

Medium

  • Sources/App/NativeTmuxSessionCoordinator.swift (surfaceDidClose, failSurfaceLaunch): SSH attachments are released without cancelling and draining serialized sizing transitions, allowing pending refresh-client operations to race with released or replacement connections. Cancel and await sizing transitions before release, as removeHandle and shutdown do.

  • Sources/App/WorkspaceSceneModel.swift:10528-10539 and Sources/App/NativeTmuxSessionCoordinator.swift:737-793: Exact-client sizing is attempted for POSIX presentations even when the tmux version lacks the required capability, potentially causing hiding to fail and detaching usable clients. Gate hidden sizing and reconnect-time ignore-size configuration on a supported-client-sizing capability.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 13m54s

A closing surface could start its replacement while an exact-client sizing
command was still running. Older tmux versions could also receive sizing
operations that depend on safe client targeting they do not provide.

Make replacement attachment work wait for canceled sizing transitions. Gate
hidden and reconnect sizing on the resolved capability, while keeping older
tmux versions available for ordinary interactive use.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 04:15 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 04:15 — with GitHub Actions Active
@wesm

wesm commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Fixed in f8a19ad.

  • Surface close and launch failure now cancel and drain serialized sizing work before SSH release.
  • Replacement attachments wait for that drain before acquiring their connection, so stale refresh-client work cannot overlap a new client.
  • Hidden and reconnect-time non-sizing configuration is now gated on safe exact-client targeting. Older tmux versions remain usable interactively and detach when hidden.
  • Regression coverage exercises all four teardown paths plus tmux 3.3 attach/hide behavior.

make swift-test (1,971 tests), make test-essential-workflows, make build, and make format pass locally. The pre-commit warnings-as-errors build also passes.

@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 23:51 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 23:51 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 2, 2026 23:52 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (b9466b3)

Verdict: Medium-severity issue found; otherwise no concrete security vulnerabilities identified.

Medium

  • Sources/App/WorkspaceSceneModel.swift:4797, 4834 — A failed worktree removal during active KWT provisioning can lose the active presentation. wasActive checks only activeBorrowedTmuxHandle, but pending activation leaves that handle nil, causing restoration to be skipped. Treat a matching pending active selection as active and restore the presentation after failed removal.

Reviewers: 2 done | Synthesis: codex, 5s | Total: 16m19s

Reactivating a hidden retained tmux client stages the selection as
active while the interactive sizing change is still in flight; the
active handle is only committed once that change lands. A worktree
removal that starts in that window recorded the presentation as
inactive, so a failed removal either restored it hidden or, for a
client that kwt must establish, did not restore it at all. The user's
open was silently lost.

The removal snapshot now treats a matching pending activation as
active, so a failed removal brings the session back in front of the
user as long as they have not navigated elsewhere since.

Generated with Claude Code (claude-fable-5-1)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 00:23 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 00:23 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 00:23 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (3599eb7)

Verdict: One medium-severity tmux reconnect race remains; otherwise, no issues were identified.

Medium

  • Sources/App/WorkspaceSceneModel.swift:10449-10472 — If the tmux client disconnects while interactive sizing is being promoted, the sizing task may fail and invalidate the retained presentation, potentially canceling reconnect before the replacement attachment is ready. Treat sizing as reconnect-pending or resume it after replacement attachment.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 11m47s

A tmux disconnect could interrupt interactive sizing while a hidden client
was being reopened. The stale sizing attempt then discarded the retained
presentation before its replacement attachment was ready.

Keep the activation pending across the disconnect and resume sizing only when
the replacement attachment can take ownership.

Generated with Codex

Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:03 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:03 — with GitHub Actions Active
@wesm

wesm commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Addressed the reconnect race in dc91059.

  • A disconnect now cancels the obsolete interactive-sizing task without clearing its pending activation revision.
  • Task cleanup no longer re-drives activation while the attachment is still closed; replacement readiness resumes sizing and activates the retained presentation.
  • The regression test blocks both sizing and reconnect, verifies that the original retained handle survives, then verifies activation after the replacement connects.

The regression failed against 3599eb7 by replacing the retained handle and passes after the fix. Local checks pass: make format, the full Swift suite (1,989 tests in 190 suites plus 213 XCTest cases), make test-essential-workflows, and make build.

@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:04 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (dc91059)

Verdict: Two issues found—one High and one Medium severity.

High

  • Sources/App/WorkspaceSceneModel.swift:10572-10635 — Hidden sizing may target the default tmux server when a protected session’s socket name is unresolved, potentially detaching the legitimate protected client. Treat such sessions as non-previewable or resolve the protected socket before issuing client-targeted commands.

Medium

  • Sources/App/WorkspaceSceneModel.swift:4829-4848 — Inactive Windows/psmux presentations restored with startsHidden: true are automatically attached without a non-sizing mode, potentially resizing shared sessions. Skip inactive Windows restorations and require explicit opening.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 16m42s

Detach protected sessions whose socket is unresolved before hidden sizing can
target the default server. Leave inactive Windows sessions detached during
restoration because psmux cannot opt a client out of shared sizing.
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:37 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:37 — with GitHub Actions Active
@wesm

wesm commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Addressed both findings in 7368598:

  • Protected tmux presentations with an unresolved socket now detach when deactivated, before any exact-client sizing command can fall back to the default server.
  • Inactive Windows restorations now remain detached because psmux has no non-sizing client mode; an explicit open still creates the interactive attachment.
  • Added focused regressions for both paths and updated the existing socketless-protected restoration coverage for the new detach-on-deactivation behavior.

Verified with make format, the complete tmux presentation/discovery suite (253 tests), the complete worktree-removal suite (72 tests), make test-essential-workflows, make swift-test (1,991 Swift Testing tests plus 213 XCTest tests, with five expected headless skips), and make build.

@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 02:37 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (7368598)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 22m38s

@wesm wesm self-assigned this Sep 3, 2026
wesm and others added 2 commits September 3, 2026 06:24
Opening a session on a host that no longer resolves, such as a removed
host or one without an SSH destination, moved the active selection but
skipped the hidden-sizing step for the client the user just left. That
client stayed retained, invisible, and still sizing the shared tmux
session, which is the exact problem this branch removes.

The unresolvable-host branch now runs the same deactivation as every
other activation path, so the previous client is told to ignore its
size before the selection changes.

Also stop a launch failure from writing its closure and disconnect
report for a handle that was closed while the SSH release was pending,
and drop the redundant POSIX-only hidden flag that the Windows guard
above it already guarantees.

Generated with Claude Code (claude-fable-5-1)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The preview-sizing test listed tmux clients with a tab between the tty
and its flags. tmux 3.7 prints that tab as an underscore unless the
process runs with a UTF-8 locale, so the test failed on every run in a
shell without LANG set even though the client flag was applied. The
app is unaffected because it runs tmux through the user's login shell.

Use a pipe separator so the check does not depend on the locale.

Generated with Claude Code (claude-fable-5-1)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 11:30 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 11:30 — with GitHub Actions Active
@wesm
wesm deployed to sandbox-image-promotion-status September 3, 2026 11:31 — with GitHub Actions Active
@roborev-ci

roborev-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (b39af3c)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 24m9s

@wesm
wesm merged commit c61c4f0 into main Sep 3, 2026
9 checks passed
@wesm
wesm deleted the fix/hidden-tmux-client-sizing branch September 3, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant