diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 33a3e2c9..6d375a6a 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -631,7 +631,26 @@ export function Config({ value: String(copyOnSelect) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS }); } - }] : []), + }] : []), { + id: 'flickerFreeMode', + label: 'Flicker-free mode', + value: globalConfig.flickerFreeMode ?? (process.env.USER_TYPE === 'ant'), + type: 'boolean' as const, + onChange(flickerFreeMode: boolean) { + saveGlobalConfig(current => ({ + ...current, + flickerFreeMode + })); + setGlobalConfig({ + ...getGlobalConfig(), + flickerFreeMode + }); + logEvent('tengu_config_changed', { + setting: 'flickerFreeMode' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + value: String(flickerFreeMode) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS + }); + } + }, // autoUpdates setting is hidden - use DISABLE_AUTOUPDATER env var to control autoUpdaterDisabledReason ? { id: 'autoUpdatesChannel', diff --git a/src/utils/config.ts b/src/utils/config.ts index 1633edf4..452749aa 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -464,6 +464,11 @@ export type GlobalConfig = { // Fullscreen in-app text selection behavior copyOnSelect?: boolean // Auto-copy to clipboard on mouse-up (undefined → true; lets cmd+c "work" via no-op) + // Flicker-free fullscreen mode (equivalent to CLAUDE_CODE_NO_FLICKER=1 env var). + // When true, enables alt-screen + virtualized scroll for all users. + // Env var still takes precedence: =0 always off, =1 always on. + flickerFreeMode?: boolean + // GitHub repo path mapping for teleport directory switching // Key: "owner/repo" (lowercase), Value: array of absolute paths where repo is cloned githubRepoPaths?: Record @@ -659,6 +664,7 @@ export const GLOBAL_CONFIG_KEYS = [ 'lspRecommendationIgnoredCount', 'copyFullResponse', 'copyOnSelect', + 'flickerFreeMode', 'permissionExplainerEnabled', 'prStatusFooterEnabled', 'remoteControlAtStartup', diff --git a/src/utils/fullscreen.ts b/src/utils/fullscreen.ts index 9d344177..33cf0930 100644 --- a/src/utils/fullscreen.ts +++ b/src/utils/fullscreen.ts @@ -1,5 +1,6 @@ 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' @@ -105,14 +106,21 @@ export function _resetTmuxControlModeProbeForTesting(): void { } /** - * Runtime env-var check only. Ants default to on (CLAUDE_CODE_NO_FLICKER=0 - * to opt out); external users default to off (CLAUDE_CODE_NO_FLICKER=1 to - * opt in). + * Whether fullscreen (flicker-free) mode is enabled. Env var takes highest + * precedence, then the `flickerFreeMode` config setting, then the ant-only + * default. External 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 + * USER_TYPE=ant → on by default for internal users */ export function isFullscreenEnvEnabled(): boolean { - // Explicit user opt-out always wins. + // Explicit env opt-out always wins. if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_NO_FLICKER)) return false - // Explicit opt-in overrides auto-detection (escape hatch). + // 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. @@ -125,6 +133,10 @@ export function isFullscreenEnvEnabled(): boolean { } 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 process.env.USER_TYPE === 'ant' }