source dump of claude code
at main 202 lines 8.6 kB view raw
1import { spawnSync } from 'child_process' 2import { getIsInteractive } from '../bootstrap/state.js' 3import { logForDebugging } from './debug.js' 4import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js' 5import { execFileNoThrow } from './execFileNoThrow.js' 6 7let loggedTmuxCcDisable = false 8let checkedTmuxMouseHint = false 9 10/** 11 * Cached result from `tmux display-message -p '#{client_control_mode}'`. 12 * undefined = not yet queried (or probe failed) — env heuristic stays authoritative. 13 */ 14let tmuxControlModeProbed: boolean | undefined 15 16/** 17 * Env-var heuristic for iTerm2's tmux integration mode (`tmux -CC` / `tmux -2CC`). 18 * 19 * In `-CC` mode, iTerm2 renders tmux panes as native splits — tmux runs 20 * as a server (TMUX is set) but iTerm2 is the actual terminal emulator 21 * for each pane, so TERM_PROGRAM stays `iTerm.app` and TERM is iTerm2's 22 * default (xterm-*). Contrast with regular tmux-inside-iTerm2, where tmux 23 * overwrites TERM_PROGRAM to `tmux` and sets TERM to screen-* or tmux-*. 24 * 25 * This heuristic has known holes (SSH often doesn't propagate TERM_PROGRAM; 26 * .tmux.conf can override TERM) — probeTmuxControlModeSync() is the 27 * authoritative backstop. Kept as a zero-subprocess fast path. 28 */ 29function isTmuxControlModeEnvHeuristic(): boolean { 30 if (!process.env.TMUX) return false 31 if (process.env.TERM_PROGRAM !== 'iTerm.app') return false 32 // Belt-and-suspenders: in regular tmux TERM is screen-* or tmux-*; 33 // in -CC mode iTerm2 sets its own TERM (xterm-*). 34 const term = process.env.TERM ?? '' 35 return !term.startsWith('screen') && !term.startsWith('tmux') 36} 37 38/** 39 * Sync one-shot probe: asks tmux directly whether this client is in control 40 * mode via `#{client_control_mode}`. Runs on first isTmuxControlMode() call 41 * when the env heuristic can't decide; result is cached. 42 * 43 * Sync (spawnSync) because the answer gates whether we enter fullscreen — an 44 * async probe raced against React render and lost: coder-tmux (ssh → tmux -CC 45 * on a remote box) doesn't propagate TERM_PROGRAM, so the env heuristic missed, 46 * and by the time the async probe resolved we'd already entered alt-screen with 47 * mouse tracking enabled. Mouse wheel is dead in iTerm2's -CC integration, so 48 * users couldn't scroll at all. 49 * 50 * Cost: one ~5ms subprocess, only when $TMUX is set AND $TERM_PROGRAM is unset 51 * (the SSH-into-tmux case). Local iTerm2 -CC and non-tmux paths skip the spawn. 52 * 53 * The TMUX env check MUST come first — without it, display-message would 54 * query whatever tmux server happens to be running rather than our client. 55 */ 56function probeTmuxControlModeSync(): void { 57 // Seed cache with heuristic result so early returns below don't leave it 58 // undefined — isTmuxControlMode() is called 15+ times per render, and an 59 // undefined cache would re-enter this function (re-spawning tmux in the 60 // failure case) on every call. 61 tmuxControlModeProbed = isTmuxControlModeEnvHeuristic() 62 if (tmuxControlModeProbed) return 63 if (!process.env.TMUX) return 64 // Only probe when iTerm might be involved: TERM_PROGRAM is iTerm.app 65 // (covered above) or not set (SSH often doesn't propagate it). When 66 // TERM_PROGRAM is explicitly a non-iTerm terminal, skip — tmux -CC is 67 // an iTerm-only feature, so the subprocess would be wasted. 68 if (process.env.TERM_PROGRAM) return 69 let result 70 try { 71 result = spawnSync( 72 'tmux', 73 ['display-message', '-p', '#{client_control_mode}'], 74 { encoding: 'utf8', timeout: 2000 }, 75 ) 76 } catch { 77 // spawnSync can throw on some platforms (e.g. ENOENT on Windows if tmux 78 // is absent and the runtime surfaces it as an exception rather than in 79 // result.error). Treat the same as a non-zero exit. 80 return 81 } 82 // Non-zero exit / spawn error: tmux too old (format var added in 2.4) or 83 // unavailable. Keep the heuristic result cached. 84 if (result.status !== 0) return 85 tmuxControlModeProbed = result.stdout.trim() === '1' 86} 87 88/** 89 * True when running under `tmux -CC` (iTerm2 integration mode). 90 * 91 * The alt-screen / mouse-tracking path in fullscreen mode is unrecoverable 92 * in -CC mode (double-click corrupts terminal state; mouse wheel is dead), 93 * so callers auto-disable fullscreen. 94 * 95 * Lazily probes tmux on first call when the env heuristic can't decide. 96 */ 97export function isTmuxControlMode(): boolean { 98 if (tmuxControlModeProbed === undefined) probeTmuxControlModeSync() 99 return tmuxControlModeProbed ?? false 100} 101 102export function _resetTmuxControlModeProbeForTesting(): void { 103 tmuxControlModeProbed = undefined 104 loggedTmuxCcDisable = false 105} 106 107/** 108 * Runtime env-var check only. Ants default to on (CLAUDE_CODE_NO_FLICKER=0 109 * to opt out); external users default to off (CLAUDE_CODE_NO_FLICKER=1 to 110 * opt in). 111 */ 112export function isFullscreenEnvEnabled(): boolean { 113 // Explicit user opt-out always wins. 114 if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_NO_FLICKER)) return false 115 // Explicit opt-in overrides auto-detection (escape hatch). 116 if (isEnvTruthy(process.env.CLAUDE_CODE_NO_FLICKER)) return true 117 // Auto-disable under tmux -CC: alt-screen + mouse tracking corrupts 118 // terminal state on double-click and mouse wheel is dead. 119 if (isTmuxControlMode()) { 120 if (!loggedTmuxCcDisable) { 121 loggedTmuxCcDisable = true 122 logForDebugging( 123 'fullscreen disabled: tmux -CC (iTerm2 integration mode) detected · set CLAUDE_CODE_NO_FLICKER=1 to override', 124 ) 125 } 126 return false 127 } 128 return process.env.USER_TYPE === 'ant' 129} 130 131/** 132 * Whether fullscreen mode should enable SGR mouse tracking (DEC 1000/1002/1006). 133 * Set CLAUDE_CODE_DISABLE_MOUSE=1 to keep alt-screen + virtualized scroll 134 * (keyboard PgUp/PgDn/Ctrl+Home/End still work) but skip mouse capture, 135 * so tmux/kitty/terminal-native copy-on-select keeps working. 136 * 137 * Compare with CLAUDE_CODE_NO_FLICKER=0 which is all-or-nothing — it also 138 * disables alt-screen and virtualized scrollback. 139 */ 140export function isMouseTrackingEnabled(): boolean { 141 return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MOUSE) 142} 143 144/** 145 * Whether mouse click handling is disabled (clicks/drags ignored, wheel still 146 * works). Set CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1 to prevent accidental clicks 147 * from triggering cursor positioning, text selection, or message expansion. 148 * 149 * Fullscreen-specific — only reachable when CLAUDE_CODE_NO_FLICKER is active. 150 */ 151export function isMouseClicksDisabled(): boolean { 152 return isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MOUSE_CLICKS) 153} 154 155/** 156 * True when the fullscreen alt-screen layout is actually rendering — 157 * requires an interactive REPL session AND the env var not explicitly 158 * set falsy. Headless paths (--print, SDK, in-process teammates) never 159 * enter fullscreen, so features that depend on alt-screen re-rendering 160 * should gate on this. 161 */ 162export function isFullscreenActive(): boolean { 163 return getIsInteractive() && isFullscreenEnvEnabled() 164} 165 166/** 167 * One-time hint for tmux users in fullscreen with `mouse off`. 168 * 169 * tmux's `mouse` option is session-scoped by design — there is no 170 * pane-level equivalent. We used to `tmux set mouse on` when entering 171 * alt-screen so wheel scrolling worked, but that changed mouse behavior 172 * for every sibling pane (vim, less, shell) and leaked on kill-pane or 173 * when multiple CC instances raced on restore. Now we leave tmux state 174 * alone — same as vim/less/htop — and just tell the user their options. 175 * 176 * Fire-and-forget from REPL startup. Returns the hint text once per 177 * session if TMUX is set, fullscreen is active, and tmux's current 178 * `mouse` option is off; null otherwise. 179 */ 180export async function maybeGetTmuxMouseHint(): Promise<string | null> { 181 if (!process.env.TMUX) return null 182 // tmux -CC auto-disables fullscreen above, but belt-and-suspenders. 183 if (!isFullscreenActive() || isTmuxControlMode()) return null 184 if (checkedTmuxMouseHint) return null 185 checkedTmuxMouseHint = true 186 // -A includes inherited values: `show -v mouse` returns empty when the 187 // option is set globally (`set -g mouse on` in .tmux.conf) but not at 188 // session level — which is the common case. -A gives the effective value. 189 const { stdout, code } = await execFileNoThrow( 190 'tmux', 191 ['show', '-Av', 'mouse'], 192 { useCwd: false, timeout: 2000 }, 193 ) 194 if (code !== 0 || stdout.trim() === 'on') return null 195 return "tmux detected · scroll with PgUp/PgDn · or add 'set -g mouse on' to ~/.tmux.conf for wheel scroll" 196} 197 198/** Test-only: reset module-level once-per-session flags. */ 199export function _resetForTesting(): void { 200 loggedTmuxCcDisable = false 201 checkedTmuxMouseHint = false 202}