source dump of claude code
at main 248 lines 8.2 kB view raw
1import { coerce } from 'semver' 2import type { Writable } from 'stream' 3import { env } from '../utils/env.js' 4import { gte } from '../utils/semver.js' 5import { getClearTerminalSequence } from './clearTerminal.js' 6import type { Diff } from './frame.js' 7import { cursorMove, cursorTo, eraseLines } from './termio/csi.js' 8import { BSU, ESU, HIDE_CURSOR, SHOW_CURSOR } from './termio/dec.js' 9import { link } from './termio/osc.js' 10 11export type Progress = { 12 state: 'running' | 'completed' | 'error' | 'indeterminate' 13 percentage?: number 14} 15 16/** 17 * Checks if the terminal supports OSC 9;4 progress reporting. 18 * Supported terminals: 19 * - ConEmu (Windows) - all versions 20 * - Ghostty 1.2.0+ 21 * - iTerm2 3.6.6+ 22 * 23 * Note: Windows Terminal interprets OSC 9;4 as notifications, not progress. 24 */ 25export function isProgressReportingAvailable(): boolean { 26 // Only available if we have a TTY (not piped) 27 if (!process.stdout.isTTY) { 28 return false 29 } 30 31 // Explicitly exclude Windows Terminal, which interprets OSC 9;4 as 32 // notifications rather than progress indicators 33 if (process.env.WT_SESSION) { 34 return false 35 } 36 37 // ConEmu supports OSC 9;4 for progress (all versions) 38 if ( 39 process.env.ConEmuANSI || 40 process.env.ConEmuPID || 41 process.env.ConEmuTask 42 ) { 43 return true 44 } 45 46 const version = coerce(process.env.TERM_PROGRAM_VERSION) 47 if (!version) { 48 return false 49 } 50 51 // Ghostty 1.2.0+ supports OSC 9;4 for progress 52 // https://ghostty.org/docs/install/release-notes/1-2-0 53 if (process.env.TERM_PROGRAM === 'ghostty') { 54 return gte(version.version, '1.2.0') 55 } 56 57 // iTerm2 3.6.6+ supports OSC 9;4 for progress 58 // https://iterm2.com/downloads.html 59 if (process.env.TERM_PROGRAM === 'iTerm.app') { 60 return gte(version.version, '3.6.6') 61 } 62 63 return false 64} 65 66/** 67 * Checks if the terminal supports DEC mode 2026 (synchronized output). 68 * When supported, BSU/ESU sequences prevent visible flicker during redraws. 69 */ 70export function isSynchronizedOutputSupported(): boolean { 71 // tmux parses and proxies every byte but doesn't implement DEC 2026. 72 // BSU/ESU pass through to the outer terminal but tmux has already 73 // broken atomicity by chunking. Skip to save 16 bytes/frame + parser work. 74 if (process.env.TMUX) return false 75 76 const termProgram = process.env.TERM_PROGRAM 77 const term = process.env.TERM 78 79 // Modern terminals with known DEC 2026 support 80 if ( 81 termProgram === 'iTerm.app' || 82 termProgram === 'WezTerm' || 83 termProgram === 'WarpTerminal' || 84 termProgram === 'ghostty' || 85 termProgram === 'contour' || 86 termProgram === 'vscode' || 87 termProgram === 'alacritty' 88 ) { 89 return true 90 } 91 92 // kitty sets TERM=xterm-kitty or KITTY_WINDOW_ID 93 if (term?.includes('kitty') || process.env.KITTY_WINDOW_ID) return true 94 95 // Ghostty may set TERM=xterm-ghostty without TERM_PROGRAM 96 if (term === 'xterm-ghostty') return true 97 98 // foot sets TERM=foot or TERM=foot-extra 99 if (term?.startsWith('foot')) return true 100 101 // Alacritty may set TERM containing 'alacritty' 102 if (term?.includes('alacritty')) return true 103 104 // Zed uses the alacritty_terminal crate which supports DEC 2026 105 if (process.env.ZED_TERM) return true 106 107 // Windows Terminal 108 if (process.env.WT_SESSION) return true 109 110 // VTE-based terminals (GNOME Terminal, Tilix, etc.) since VTE 0.68 111 const vteVersion = process.env.VTE_VERSION 112 if (vteVersion) { 113 const version = parseInt(vteVersion, 10) 114 if (version >= 6800) return true 115 } 116 117 return false 118} 119 120// -- XTVERSION-detected terminal name (populated async at startup) -- 121// 122// TERM_PROGRAM is not forwarded over SSH by default, so env-based detection 123// fails when claude runs remotely inside a VS Code integrated terminal. 124// XTVERSION (CSI > 0 q → DCS > | name ST) goes through the pty — the query 125// reaches the *client* terminal and the reply comes back through stdin. 126// App.tsx fires the query when raw mode enables; setXtversionName() is called 127// from the response handler. Readers should treat undefined as "not yet known" 128// and fall back to env-var detection. 129 130let xtversionName: string | undefined 131 132/** Record the XTVERSION response. Called once from App.tsx when the reply 133 * arrives on stdin. No-op if already set (defend against re-probe). */ 134export function setXtversionName(name: string): void { 135 if (xtversionName === undefined) xtversionName = name 136} 137 138/** True if running in an xterm.js-based terminal (VS Code, Cursor, Windsurf 139 * integrated terminals). Combines TERM_PROGRAM env check (fast, sync, but 140 * not forwarded over SSH) with the XTVERSION probe result (async, survives 141 * SSH — query/reply goes through the pty). Early calls may miss the probe 142 * reply — call lazily (e.g. in an event handler) if SSH detection matters. */ 143export function isXtermJs(): boolean { 144 if (process.env.TERM_PROGRAM === 'vscode') return true 145 return xtversionName?.startsWith('xterm.js') ?? false 146} 147 148// Terminals known to correctly implement the Kitty keyboard protocol 149// (CSI >1u) and/or xterm modifyOtherKeys (CSI >4;2m) for ctrl+shift+<letter> 150// disambiguation. We previously enabled unconditionally (#23350), assuming 151// terminals silently ignore unknown CSI — but some terminals honor the enable 152// and emit codepoints our input parser doesn't handle (notably over SSH and 153// in xterm.js-based terminals like VS Code). tmux is allowlisted because it 154// accepts modifyOtherKeys and doesn't forward the kitty sequence to the outer 155// terminal. 156const EXTENDED_KEYS_TERMINALS = [ 157 'iTerm.app', 158 'kitty', 159 'WezTerm', 160 'ghostty', 161 'tmux', 162 'windows-terminal', 163] 164 165/** True if this terminal correctly handles extended key reporting 166 * (Kitty keyboard protocol + xterm modifyOtherKeys). */ 167export function supportsExtendedKeys(): boolean { 168 return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '') 169} 170 171/** True if the terminal scrolls the viewport when it receives cursor-up 172 * sequences that reach above the visible area. On Windows, conhost's 173 * SetConsoleCursorPosition follows the cursor into scrollback 174 * (microsoft/terminal#14774), yanking users to the top of their buffer 175 * mid-stream. WT_SESSION catches WSL-in-Windows-Terminal where platform 176 * is linux but output still routes through conhost. */ 177export function hasCursorUpViewportYankBug(): boolean { 178 return process.platform === 'win32' || !!process.env.WT_SESSION 179} 180 181// Computed once at module load — terminal capabilities don't change mid-session. 182// Exported so callers can pass a sync-skip hint gated to specific modes. 183export const SYNC_OUTPUT_SUPPORTED = isSynchronizedOutputSupported() 184 185export type Terminal = { 186 stdout: Writable 187 stderr: Writable 188} 189 190export function writeDiffToTerminal( 191 terminal: Terminal, 192 diff: Diff, 193 skipSyncMarkers = false, 194): void { 195 // No output if there are no patches 196 if (diff.length === 0) { 197 return 198 } 199 200 // BSU/ESU wrapping is opt-out to keep main-screen behavior unchanged. 201 // Callers pass skipSyncMarkers=true when the terminal doesn't support 202 // DEC 2026 (e.g. tmux) AND the cost matters (high-frequency alt-screen). 203 const useSync = !skipSyncMarkers 204 205 // Buffer all writes into a single string to avoid multiple write calls 206 let buffer = useSync ? BSU : '' 207 208 for (const patch of diff) { 209 switch (patch.type) { 210 case 'stdout': 211 buffer += patch.content 212 break 213 case 'clear': 214 if (patch.count > 0) { 215 buffer += eraseLines(patch.count) 216 } 217 break 218 case 'clearTerminal': 219 buffer += getClearTerminalSequence() 220 break 221 case 'cursorHide': 222 buffer += HIDE_CURSOR 223 break 224 case 'cursorShow': 225 buffer += SHOW_CURSOR 226 break 227 case 'cursorMove': 228 buffer += cursorMove(patch.x, patch.y) 229 break 230 case 'cursorTo': 231 buffer += cursorTo(patch.col) 232 break 233 case 'carriageReturn': 234 buffer += '\r' 235 break 236 case 'hyperlink': 237 buffer += link(patch.uri) 238 break 239 case 'styleStr': 240 buffer += patch.str 241 break 242 } 243 } 244 245 // Add synchronized update end and flush buffer 246 if (useSync) buffer += ESU 247 terminal.stdout.write(buffer) 248}