* feat: open useful USER_TYPE-gated features to all users Remove 13 process.env.USER_TYPE === 'ant' gates that restricted useful features to Anthropic employees. These features work without Anthropic infrastructure and are now available to all open-build users. Features opened: - Agent nesting (sub-agents can spawn sub-agents) - Effort 'max' persistence in settings - Plan mode interview phase (controlled by feature flags) - Sandbox disabled commands (via ~/.claude/feature-flags.json) - All tips visible to all users (plan mode, feedback, shift-tab) Simplified: - Fullscreen defaults to off (use /config to enable) - Explore agent always uses haiku model - Plan mode tool uses conservative prompt for all users Continues the USER_TYPE cleanup from #637 (dead code) and builds on #639 (local feature flags). * fix: address Copilot review comments — remove residual dead code 1. bridgeConfig.ts: ungate bridge override functions — return env vars directly instead of hardcoded undefined 2. bridgeMain.ts + initReplBridge.ts: ungate sessionIngressUrl — read CLAUDE_BRIDGE_SESSION_INGRESS_URL without USER_TYPE check 3. tools.ts: remove dead ConfigTool/TungstenTool imports, narrow eslint-disable scope, stub REPLTool/SuggestBackgroundPRTool to null 4. readOnlyValidation.ts: remove orphaned ANT_ONLY_COMMAND_ALLOWLIST and unused GH_READ_ONLY_COMMANDS import 5. insights.ts: remove entire remote collection plumbing (types, functions, options, display logic) 6. osc.ts: hardcode supportsTabStatus() to false (internal-only feature) 7. state.ts: simplify addSlowOperation/getSlowOperations to no-ops, remove dead constants * fix: address Copilot review on PR #644 1. settings/types.ts: allow 'max' effort level for all users in Zod schema — was still gated behind USER_TYPE=ant, causing 'max' to be silently dropped on settings reload 2. shouldUseSandbox.ts: defensively normalize disabledCommands from feature flag config with Array.isArray() guards * fix: address second round of Copilot review on PR #644 1. shouldUseSandbox.ts: validate top-level shape of disabledCommands before accessing properties (handles null/primitive from feature flag) 2. fullscreen.ts: update JSDoc to reflect removal of USER_TYPE default 3. osc.ts: update JSDoc — "Ant-only" → "Currently disabled"
215 lines
9.0 KiB
TypeScript
215 lines
9.0 KiB
TypeScript
import { spawnSync } from 'child_process'
|
|
import { getIsInteractive } from '../bootstrap/state.js'
|
|
import { getGlobalConfig } from './config.js'
|
|
import { logForDebugging } from './debug.js'
|
|
import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js'
|
|
import { execFileNoThrow } from './execFileNoThrow.js'
|
|
|
|
let loggedTmuxCcDisable = false
|
|
let checkedTmuxMouseHint = false
|
|
|
|
/**
|
|
* Cached result from `tmux display-message -p '#{client_control_mode}'`.
|
|
* undefined = not yet queried (or probe failed) — env heuristic stays authoritative.
|
|
*/
|
|
let tmuxControlModeProbed: boolean | undefined
|
|
|
|
/**
|
|
* Env-var heuristic for iTerm2's tmux integration mode (`tmux -CC` / `tmux -2CC`).
|
|
*
|
|
* In `-CC` mode, iTerm2 renders tmux panes as native splits — tmux runs
|
|
* as a server (TMUX is set) but iTerm2 is the actual terminal emulator
|
|
* for each pane, so TERM_PROGRAM stays `iTerm.app` and TERM is iTerm2's
|
|
* default (xterm-*). Contrast with regular tmux-inside-iTerm2, where tmux
|
|
* overwrites TERM_PROGRAM to `tmux` and sets TERM to screen-* or tmux-*.
|
|
*
|
|
* This heuristic has known holes (SSH often doesn't propagate TERM_PROGRAM;
|
|
* .tmux.conf can override TERM) — probeTmuxControlModeSync() is the
|
|
* authoritative backstop. Kept as a zero-subprocess fast path.
|
|
*/
|
|
function isTmuxControlModeEnvHeuristic(): boolean {
|
|
if (!process.env.TMUX) return false
|
|
if (process.env.TERM_PROGRAM !== 'iTerm.app') return false
|
|
// Belt-and-suspenders: in regular tmux TERM is screen-* or tmux-*;
|
|
// in -CC mode iTerm2 sets its own TERM (xterm-*).
|
|
const term = process.env.TERM ?? ''
|
|
return !term.startsWith('screen') && !term.startsWith('tmux')
|
|
}
|
|
|
|
/**
|
|
* Sync one-shot probe: asks tmux directly whether this client is in control
|
|
* mode via `#{client_control_mode}`. Runs on first isTmuxControlMode() call
|
|
* when the env heuristic can't decide; result is cached.
|
|
*
|
|
* Sync (spawnSync) because the answer gates whether we enter fullscreen — an
|
|
* async probe raced against React render and lost: coder-tmux (ssh → tmux -CC
|
|
* on a remote box) doesn't propagate TERM_PROGRAM, so the env heuristic missed,
|
|
* and by the time the async probe resolved we'd already entered alt-screen with
|
|
* mouse tracking enabled. Mouse wheel is dead in iTerm2's -CC integration, so
|
|
* users couldn't scroll at all.
|
|
*
|
|
* Cost: one ~5ms subprocess, only when $TMUX is set AND $TERM_PROGRAM is unset
|
|
* (the SSH-into-tmux case). Local iTerm2 -CC and non-tmux paths skip the spawn.
|
|
*
|
|
* The TMUX env check MUST come first — without it, display-message would
|
|
* query whatever tmux server happens to be running rather than our client.
|
|
*/
|
|
function probeTmuxControlModeSync(): void {
|
|
// Seed cache with heuristic result so early returns below don't leave it
|
|
// undefined — isTmuxControlMode() is called 15+ times per render, and an
|
|
// undefined cache would re-enter this function (re-spawning tmux in the
|
|
// failure case) on every call.
|
|
tmuxControlModeProbed = isTmuxControlModeEnvHeuristic()
|
|
if (tmuxControlModeProbed) return
|
|
if (!process.env.TMUX) return
|
|
// Only probe when iTerm might be involved: TERM_PROGRAM is iTerm.app
|
|
// (covered above) or not set (SSH often doesn't propagate it). When
|
|
// TERM_PROGRAM is explicitly a non-iTerm terminal, skip — tmux -CC is
|
|
// an iTerm-only feature, so the subprocess would be wasted.
|
|
if (process.env.TERM_PROGRAM) return
|
|
let result
|
|
try {
|
|
result = spawnSync(
|
|
'tmux',
|
|
['display-message', '-p', '#{client_control_mode}'],
|
|
{ encoding: 'utf8', timeout: 2000 },
|
|
)
|
|
} catch {
|
|
// spawnSync can throw on some platforms (e.g. ENOENT on Windows if tmux
|
|
// is absent and the runtime surfaces it as an exception rather than in
|
|
// result.error). Treat the same as a non-zero exit.
|
|
return
|
|
}
|
|
// Non-zero exit / spawn error: tmux too old (format var added in 2.4) or
|
|
// unavailable. Keep the heuristic result cached.
|
|
if (result.status !== 0) return
|
|
tmuxControlModeProbed = result.stdout.trim() === '1'
|
|
}
|
|
|
|
/**
|
|
* True when running under `tmux -CC` (iTerm2 integration mode).
|
|
*
|
|
* The alt-screen / mouse-tracking path in fullscreen mode is unrecoverable
|
|
* in -CC mode (double-click corrupts terminal state; mouse wheel is dead),
|
|
* so callers auto-disable fullscreen.
|
|
*
|
|
* Lazily probes tmux on first call when the env heuristic can't decide.
|
|
*/
|
|
export function isTmuxControlMode(): boolean {
|
|
if (tmuxControlModeProbed === undefined) probeTmuxControlModeSync()
|
|
return tmuxControlModeProbed ?? false
|
|
}
|
|
|
|
export function _resetTmuxControlModeProbeForTesting(): void {
|
|
tmuxControlModeProbed = undefined
|
|
loggedTmuxCcDisable = false
|
|
}
|
|
|
|
/**
|
|
* Whether fullscreen (flicker-free) mode is enabled. Env var takes highest
|
|
* precedence, then the `flickerFreeMode` config setting, then defaults to off.
|
|
* Users can enable via `/config` instead of setting the env.
|
|
*
|
|
* Priority order:
|
|
* CLAUDE_CODE_NO_FLICKER=0 → always off
|
|
* CLAUDE_CODE_NO_FLICKER=1 → always on (overrides tmux -CC guard too)
|
|
* tmux -CC detected → off (corrupts terminal state)
|
|
* config flickerFreeMode → on/off per user preference
|
|
* default → off
|
|
*/
|
|
export function isFullscreenEnvEnabled(): boolean {
|
|
// Explicit env opt-out always wins.
|
|
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_NO_FLICKER)) return false
|
|
// Explicit env opt-in overrides everything including tmux -CC.
|
|
if (isEnvTruthy(process.env.CLAUDE_CODE_NO_FLICKER)) return true
|
|
// Auto-disable under tmux -CC: alt-screen + mouse tracking corrupts
|
|
// terminal state on double-click and mouse wheel is dead.
|
|
if (isTmuxControlMode()) {
|
|
if (!loggedTmuxCcDisable) {
|
|
loggedTmuxCcDisable = true
|
|
logForDebugging(
|
|
'fullscreen disabled: tmux -CC (iTerm2 integration mode) detected · set CLAUDE_CODE_NO_FLICKER=1 to override',
|
|
)
|
|
}
|
|
return false
|
|
}
|
|
// Config-based toggle: lets external users enable flicker-free mode via
|
|
// `/config` without having to set an env var.
|
|
const configValue = getGlobalConfig().flickerFreeMode
|
|
if (configValue !== undefined) return configValue
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Whether fullscreen mode should enable SGR mouse tracking (DEC 1000/1002/1006).
|
|
* Set CLAUDE_CODE_DISABLE_MOUSE=1 to keep alt-screen + virtualized scroll
|
|
* (keyboard PgUp/PgDn/Ctrl+Home/End still work) but skip mouse capture,
|
|
* so tmux/kitty/terminal-native copy-on-select keeps working.
|
|
*
|
|
* Compare with CLAUDE_CODE_NO_FLICKER=0 which is all-or-nothing — it also
|
|
* disables alt-screen and virtualized scrollback.
|
|
*/
|
|
export function isMouseTrackingEnabled(): boolean {
|
|
return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MOUSE)
|
|
}
|
|
|
|
/**
|
|
* Whether mouse click handling is disabled (clicks/drags ignored, wheel still
|
|
* works). Set CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1 to prevent accidental clicks
|
|
* from triggering cursor positioning, text selection, or message expansion.
|
|
*
|
|
* Fullscreen-specific — only reachable when CLAUDE_CODE_NO_FLICKER is active.
|
|
*/
|
|
export function isMouseClicksDisabled(): boolean {
|
|
return isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MOUSE_CLICKS)
|
|
}
|
|
|
|
/**
|
|
* True when the fullscreen alt-screen layout is actually rendering —
|
|
* requires an interactive REPL session AND the env var not explicitly
|
|
* set falsy. Headless paths (--print, SDK, in-process teammates) never
|
|
* enter fullscreen, so features that depend on alt-screen re-rendering
|
|
* should gate on this.
|
|
*/
|
|
export function isFullscreenActive(): boolean {
|
|
return getIsInteractive() && isFullscreenEnvEnabled()
|
|
}
|
|
|
|
/**
|
|
* One-time hint for tmux users in fullscreen with `mouse off`.
|
|
*
|
|
* tmux's `mouse` option is session-scoped by design — there is no
|
|
* pane-level equivalent. We used to `tmux set mouse on` when entering
|
|
* alt-screen so wheel scrolling worked, but that changed mouse behavior
|
|
* for every sibling pane (vim, less, shell) and leaked on kill-pane or
|
|
* when multiple CC instances raced on restore. Now we leave tmux state
|
|
* alone — same as vim/less/htop — and just tell the user their options.
|
|
*
|
|
* Fire-and-forget from REPL startup. Returns the hint text once per
|
|
* session if TMUX is set, fullscreen is active, and tmux's current
|
|
* `mouse` option is off; null otherwise.
|
|
*/
|
|
export async function maybeGetTmuxMouseHint(): Promise<string | null> {
|
|
if (!process.env.TMUX) return null
|
|
// tmux -CC auto-disables fullscreen above, but belt-and-suspenders.
|
|
if (!isFullscreenActive() || isTmuxControlMode()) return null
|
|
if (checkedTmuxMouseHint) return null
|
|
checkedTmuxMouseHint = true
|
|
// -A includes inherited values: `show -v mouse` returns empty when the
|
|
// option is set globally (`set -g mouse on` in .tmux.conf) but not at
|
|
// session level — which is the common case. -A gives the effective value.
|
|
const { stdout, code } = await execFileNoThrow(
|
|
'tmux',
|
|
['show', '-Av', 'mouse'],
|
|
{ useCwd: false, timeout: 2000 },
|
|
)
|
|
if (code !== 0 || stdout.trim() === 'on') return null
|
|
return "tmux detected · scroll with PgUp/PgDn · or add 'set -g mouse on' to ~/.tmux.conf for wheel scroll"
|
|
}
|
|
|
|
/** Test-only: reset module-level once-per-session flags. */
|
|
export function _resetForTesting(): void {
|
|
loggedTmuxCcDisable = false
|
|
checkedTmuxMouseHint = false
|
|
}
|