File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -294,12 +294,17 @@ enum MuxSessionDetach {
294294 return
295295 }
296296
297+ // Snapshot the live transport before clearing bindings / any close
298+ // side effects — closeTab may already be removing us from the model.
299+ let trzsz = terminal . session as? TrzszSession
300+ let connectionConfig = terminal . connectionConfig
301+
297302 // Drop the binding so a concurrent detach/close path cannot double-kill.
298303 terminal . passthroughMultiplexer = nil
299304 AgentAttentionCenter . shared . topologyDidChange ( )
300305
301306 // 1) Live tsshd probe — same connection, no re-auth.
302- if let trzsz = terminal . session as? TrzszSession {
307+ if let trzsz {
303308 do {
304309 _ = try await trzsz . runProbeCommand ( command )
305310 Ghostty . logger . info ( " zmx kill via tsshd probe succeeded for \( name , privacy : . public ) " )
@@ -311,7 +316,6 @@ enum MuxSessionDetach {
311316 }
312317
313318 // 2) Headless SSH fallback (new connection — may prompt the agent).
314- let connectionConfig = terminal . connectionConfig
315319 if let ssh = connectionConfig . sshConfigForHistory
316320 ?? connectionConfig . underlyingSSHConfig {
317321 do {
Original file line number Diff line number Diff line change @@ -804,11 +804,12 @@ struct SSHConfig: Codable, Hashable {
804804
805805 /// Best-effort destroy for an explicit tab/split close. Names must already
806806 /// pass ``isEmbeddableZmxSessionName`` (no shell quoting). `--force` kills
807- /// the session even while this client is still attached.
807+ /// the session even while this client is still attached. Wrapped in
808+ /// `sh -c` so PATH prefix / `&&` work under tsshd `runCommand` and SSH exec.
808809 static func zmxKillCommandLine ( sessionName : String ) -> String ? {
809810 guard isEmbeddableZmxSessionName ( sessionName ) else { return nil }
810- return " \( remoteExecPathPrefix ) command -v zmx >/dev/null 2>&1 "
811- + " && ZMX_SESSION_PREFIX= zmx kill --force \( sessionName ) "
811+ return " sh -c ' \( remoteExecPathPrefix ) command -v zmx >/dev/null 2>&1 "
812+ + " && ZMX_SESSION_PREFIX= exec zmx kill --force \( sessionName ) ' "
812813 }
813814
814815 /// Shared zmx exec command used by all session types.
Original file line number Diff line number Diff line change @@ -1558,9 +1558,13 @@ extension Ghostty {
15581558 // remote session (`zmx kill`) BEFORE tearing down the client —
15591559 // closing the client alone is zmx’s detach path. Prefer an
15601560 // in-band tsshd probe so agent keys do not need a second approval.
1561+ //
1562+ // Strongly capture `self`: closeTab removes the pane from the
1563+ // model immediately after cleanup returns, and a `[weak self]`
1564+ // Task would often see nil and skip both kill and teardown —
1565+ // leaving the zmx session alive (looks like detach).
15611566 if reason == . userClose , MuxSessionDetach . hasZmxSessionToDestroy ( on : self ) {
1562- Task { @MainActor [ weak self ] in
1563- guard let self else { return }
1567+ Task { @MainActor in
15641568 await MuxSessionDetach . destroyZmxSessionIfNeeded ( on : self )
15651569 self . completeCleanupAfterSessionStop ( reason : reason )
15661570 }
You can’t perform that action at this time.
0 commit comments