π― Use case
This file lives under βink/β, which covers Ink terminal UI (layouts, TTY IO, keyboard, renderer components). On the API surface it exposes Progress, isProgressReportingAvailable, isSynchronizedOutputSupported, setXtversionName, and isXtermJs (and more) β mainly functions, hooks, or classes. Dependencies touch version comparison and Node streams. It composes internal code from utils, clearTerminal, frame, and termio (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { coerce } from 'semver' import type { Writable } from 'stream' import { env } from '../utils/env.js' import { gte } from '../utils/semver.js' import { getClearTerminalSequence } from './clearTerminal.js'
π€ Exports (heuristic)
ProgressisProgressReportingAvailableisSynchronizedOutputSupportedsetXtversionNameisXtermJssupportsExtendedKeyshasCursorUpViewportYankBugSYNC_OUTPUT_SUPPORTEDTerminalwriteDiffToTerminal
π External import roots
Package roots from from "β¦" (relative paths omitted).
semverstream
π₯οΈ Source preview
import { coerce } from 'semver'
import type { Writable } from 'stream'
import { env } from '../utils/env.js'
import { gte } from '../utils/semver.js'
import { getClearTerminalSequence } from './clearTerminal.js'
import type { Diff } from './frame.js'
import { cursorMove, cursorTo, eraseLines } from './termio/csi.js'
import { BSU, ESU, HIDE_CURSOR, SHOW_CURSOR } from './termio/dec.js'
import { link } from './termio/osc.js'
export type Progress = {
state: 'running' | 'completed' | 'error' | 'indeterminate'
percentage?: number
}
/**
* Checks if the terminal supports OSC 9;4 progress reporting.
* Supported terminals:
* - ConEmu (Windows) - all versions
* - Ghostty 1.2.0+
* - iTerm2 3.6.6+
*
* Note: Windows Terminal interprets OSC 9;4 as notifications, not progress.
*/
export function isProgressReportingAvailable(): boolean {
// Only available if we have a TTY (not piped)
if (!process.stdout.isTTY) {
return false
}
// Explicitly exclude Windows Terminal, which interprets OSC 9;4 as
// notifications rather than progress indicators
if (process.env.WT_SESSION) {
return false
}
// ConEmu supports OSC 9;4 for progress (all versions)
if (
process.env.ConEmuANSI ||
process.env.ConEmuPID ||
process.env.ConEmuTask
) {
return true
}
const version = coerce(process.env.TERM_PROGRAM_VERSION)
if (!version) {
return false
}
// Ghostty 1.2.0+ supports OSC 9;4 for progress
// https://ghostty.org/docs/install/release-notes/1-2-0
if (process.env.TERM_PROGRAM === 'ghostty') {
return gte(version.version, '1.2.0')
}
// iTerm2 3.6.6+ supports OSC 9;4 for progress
// https://iterm2.com/downloads.html
if (process.env.TERM_PROGRAM === 'iTerm.app') {
return gte(version.version, '3.6.6')
}
return false
}
/**
* Checks if the terminal supports DEC mode 2026 (synchronized output).
* When supported, BSU/ESU sequences prevent visible flicker during redraws.
*/
export function isSynchronizedOutputSupported(): boolean {
// tmux parses and proxies every byte but doesn't implement DEC 2026.
// BSU/ESU pass through to the outer terminal but tmux has already
// broken atomicity by chunking. Skip to save 16 bytes/frame + parser work.
if (process.env.TMUX) return false
const termProgram = process.env.TERM_PROGRAM
const term = process.env.TERM
// Modern terminals with known DEC 2026 support
if (
termProgram === 'iTerm.app' ||
termProgram === 'WezTerm' ||
termProgram === 'WarpTerminal' ||
termProgram === 'ghostty' ||
termProgram === 'contour' ||
termProgram === 'vscode' ||
termProgram === 'alacritty'
) {
return true
}
// kitty sets TERM=xterm-kitty or KITTY_WINDOW_ID
if (term?.includes('kitty') || process.env.KITTY_WINDOW_ID) return true
// Ghostty may set TERM=xterm-ghostty without TERM_PROGRAM
if (term === 'xterm-ghostty') return true
// foot sets TERM=foot or TERM=foot-extra
if (term?.startsWith('foot')) return true
// Alacritty may set TERM containing 'alacritty'
if (term?.includes('alacritty')) return true
// Zed uses the alacritty_terminal crate which supports DEC 2026
if (process.env.ZED_TERM) return true
// Windows Terminal
if (process.env.WT_SESSION) return true
// VTE-based terminals (GNOME Terminal, Tilix, etc.) since VTE 0.68
const vteVersion = process.env.VTE_VERSION
if (vteVersion) {
const version = parseInt(vteVersion, 10)
if (version >= 6800) return true
}
return false
}
// -- XTVERSION-detected terminal name (populated async at startup) --
//
// TERM_PROGRAM is not forwarded over SSH by default, so env-based detection
// fails when claude runs remotely inside a VS Code integrated terminal.
// XTVERSION (CSI > 0 q β DCS > | name ST) goes through the pty β the query
// reaches the *client* terminal and the reply comes back through stdin.
// App.tsx fires the query when raw mode enables; setXtversionName() is called
// from the response handler. Readers should treat undefined as "not yet known"
// and fall back to env-var detection.
let xtversionName: string | undefined
/** Record the XTVERSION response. Called once from App.tsx when the reply
* arrives on stdin. No-op if already set (defend against re-probe). */
export function setXtversionName(name: string): void {
if (xtversionName === undefined) xtversionName = name
}
/** True if running in an xterm.js-based terminal (VS Code, Cursor, Windsurf
* integrated terminals). Combines TERM_PROGRAM env check (fast, sync, but
* not forwarded over SSH) with the XTVERSION probe result (async, survives
* SSH β query/reply goes through the pty). Early calls may miss the probe
* reply β call lazily (e.g. in an event handler) if SSH detection matters. */
export function isXtermJs(): boolean {
if (process.env.TERM_PROGRAM === 'vscode') return true
return xtversionName?.startsWith('xterm.js') ?? false
}
// Terminals known to correctly implement the Kitty keyboard protocol
// (CSI >1u) and/or xterm modifyOtherKeys (CSI >4;2m) for ctrl+shift+<letter>
// disambiguation. We previously enabled unconditionally (#23350), assuming
// terminals silently ignore unknown CSI β but some terminals honor the enable
// and emit codepoints our input parser doesn't handle (notably over SSH and
// in xterm.js-based terminals like VS Code). tmux is allowlisted because it
// accepts modifyOtherKeys and doesn't forward the kitty sequence to the outer
// terminal.
const EXTENDED_KEYS_TERMINALS = [
'iTerm.app',
'kitty',
'WezTerm',
'ghostty',
'tmux',
'windows-terminal',
]
/** True if this terminal correctly handles extended key reporting
* (Kitty keyboard protocol + xterm modifyOtherKeys). */
export function supportsExtendedKeys(): boolean {
return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '')
}
/** True if the terminal scrolls the viewport when it receives cursor-up
* sequences that reach above the visible area. On Windows, conhost's
* SetConsoleCursorPosition follows the cursor into scrollback
* (microsoft/terminal#14774), yanking users to the top of their buffer
* mid-stream. WT_SESSION catches WSL-in-Windows-Terminal where platform
* is linux but output still routes through conhost. */
export function hasCursorUpViewportYankBug(): boolean {
return process.platform === 'win32' || !!process.env.WT_SESSION
}
// Computed once at module load β terminal capabilities don't change mid-session.
// Exported so callers can pass a sync-skip hint gated to specific modes.
export const SYNC_OUTPUT_SUPPORTED = isSynchronizedOutputSupported()
export type Terminal = {
stdout: Writable
stderr: Writable
}
export function writeDiffToTerminal(
terminal: Terminal,
diff: Diff,
skipSyncMarkers = false,
): void {
// No output if there are no patches
if (diff.length === 0) {
return
}
// BSU/ESU wrapping is opt-out to keep main-screen behavior unchanged.
// Callers pass skipSyncMarkers=true when the terminal doesn't support
// DEC 2026 (e.g. tmux) AND the cost matters (high-frequency alt-screen).
const useSync = !skipSyncMarkers
// Buffer all writes into a single string to avoid multiple write calls
let buffer = useSync ? BSU : ''
for (const patch of diff) {
switch (patch.type) {
case 'stdout':
buffer += patch.content
break
case 'clear':
if (patch.count > 0) {
buffer += eraseLines(patch.count)
}
break
case 'clearTerminal':
buffer += getClearTerminalSequence()
break
case 'cursorHide':
buffer += HIDE_CURSOR
break
case 'cursorShow':
buffer += SHOW_CURSOR
break
case 'cursorMove':
buffer += cursorMove(patch.x, patch.y)
break
case 'cursorTo':
buffer += cursorTo(patch.col)
break
case 'carriageReturn':
buffer += '\r'
break
case 'hyperlink':
buffer += link(patch.uri)
break
case 'styleStr':
buffer += patch.str
break
}
}
// Add synchronized update end and flush buffer
if (useSync) buffer += ESU
terminal.stdout.write(buffer)
}