fix(desktop): default USE_TMUX to auto so quitting the app stops killing sessions

The desktop shell pinned USE_TMUX=0 on every platform, not just Windows. Without
tmux the shell is a direct child of the embedded server, so SessionManager.shutdown
takes the kill branch and quitting the app — the tray Quit, a logout, an update —
ends every session with it.

That defeats the app's premise. The point is to hand Claude Code a task, walk away,
and reconnect from another device; a session that cannot survive the window closing
does not do that. With tmux the node-pty process is only a tmux CLIENT, so shutdown
detaches and the shell keeps running, which is exactly what the standalone server
has always done (USE_TMUX unset → 'auto' → is the binary present?).

Non-Windows now defaults to 'auto'. Windows stays forced off — tmux is a *nix
keepalive. An explicit ambient USE_TMUX still wins either way, so USE_TMUX=0 remains
the opt-out.

Tests assert the end-to-end result, not just the emitted string: buildServerEnv fed
through loadConfig yields useTmux === true with tmux installed, and false for an
explicit opt-out. The original bug was these two layers disagreeing about what a
value meant, so checking the string alone would not have caught it.

Sessions started in the app before this change are NOT retroactively protected —
they have no tmux session behind them and will still die with the app. Only sessions
created after the new build is installed survive.
This commit is contained in:
Yaojia Wang
2026-07-30 12:47:05 +02:00
parent 3ad8d8beba
commit 5833529c0c
2 changed files with 54 additions and 4 deletions

View File

@@ -5,10 +5,22 @@
* guarantee (input.env untouched), and pass-through of unrelated env vars.
*/
import { execFileSync } from 'node:child_process'
import { describe, test, expect } from 'vitest'
import { buildServerEnv } from '../../desktop/src/server-config.js'
import { loadConfig } from '../../src/config.js'
import type { DesktopPrefs } from '../../desktop/src/types.js'
const tmuxInstalled = (() => {
try {
execFileSync('tmux', ['-V'], { stdio: 'ignore' })
return true
} catch {
return false
}
})()
const BASE_PREFS: DesktopPrefs = {
port: null,
lanSharing: false,
@@ -69,11 +81,33 @@ describe('buildServerEnv', () => {
expect(result.USE_TMUX).toBe('1')
})
test('defaults USE_TMUX to off on non-Windows when the env is unset', () => {
test('defaults USE_TMUX to auto-detect on non-Windows when the env is unset', () => {
// Not '0'. Without tmux the shell is a direct child of the embedded server, so
// quitting the app kills every session — which defeats the point of walking away
// from a task and reconnecting later. 'auto' means "use tmux if it is installed".
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'linux', env: {} })
expect(result.USE_TMUX).toBe('auto')
})
test('an explicit ambient USE_TMUX=0 still wins — opting out stays possible', () => {
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: { USE_TMUX: '0' } })
expect(result.USE_TMUX).toBe('0')
})
// The bug this guards against was the two layers disagreeing: buildServerEnv emitted
// a value that loadConfig then resolved to something else than intended. Assert the
// end-to-end result, not just the string.
test('the default resolves to tmux ON once loadConfig sees it', () => {
if (!tmuxInstalled) return // 'auto' correctly resolves to false without the binary
const env = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: {} })
expect(loadConfig({ ...env, BIND_HOST: '127.0.0.1' }).useTmux).toBe(true)
})
test('an explicit opt-out resolves to tmux OFF through loadConfig', () => {
const env = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: { USE_TMUX: '0' } })
expect(loadConfig({ ...env, BIND_HOST: '127.0.0.1' }).useTmux).toBe(false)
})
test('does not mutate the input env', () => {
// Arrange
const env = { PATH: '/usr/bin', USE_TMUX: '1' }