fix: use env-var terminal detection to avoid stdin conflicts in TUI
Browse filesReplace InkPictureProvider's ANSI escape queryTerminal() with
environment-variable-based detection (TERM, TERM_PROGRAM, KITTY_WINDOW_ID)
to reliably detect Kitty/Sixel/iTerm2 protocol support without hijacking
stdin, which conflicted with Ink's TUI input management.
InkPictureProvider now skips queryTerminal entirely when terminalInfo
overrides are provided, preventing garbled ANSI response leakage into
the input bar.
Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>
src/ink-picture/InkPictureProvider.tsx
CHANGED
|
@@ -231,6 +231,11 @@ export function InkPictureProvider({
|
|
| 231 |
}, [config]);
|
| 232 |
|
| 233 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
const controller = new AbortController();
|
| 235 |
|
| 236 |
const queryTerminalInfo = async () => {
|
|
|
|
| 231 |
}, [config]);
|
| 232 |
|
| 233 |
useEffect(() => {
|
| 234 |
+
// When terminalInfo overrides are provided, skip the ANSI escape query
|
| 235 |
+
// to avoid stdout/stden conflicts with the host TUI. The overrides take
|
| 236 |
+
// precedence over queried results in resolvedInfo anyway.
|
| 237 |
+
if (overrides && Object.keys(overrides).length > 0) return;
|
| 238 |
+
|
| 239 |
const controller = new AbortController();
|
| 240 |
|
| 241 |
const queryTerminalInfo = async () => {
|
src/tools/ImageShowTool/UI.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
import Image, { InkPictureProvider } from '../../ink-picture/index.js'
|
| 3 |
import { Box, Text } from '../../ink.js'
|
| 4 |
import { MessageResponse } from '../../components/MessageResponse.js'
|
| 5 |
import type { ImageShowOutput } from './ImageShowTool.js'
|
|
|
|
| 6 |
|
| 7 |
// ── Image display component ──
|
| 8 |
// Renders a placeholder in Ink's TUI and uses Kitty/Sixel protocol to draw
|
|
@@ -10,6 +11,9 @@ import type { ImageShowOutput } from './ImageShowTool.js'
|
|
| 10 |
// the Ink render cycle). The placeholder reserves character cells so the TUI
|
| 11 |
// layout isn't broken; ink-picture's useDirectRenderer repositions the image
|
| 12 |
// after each Ink screen refresh.
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
export function ImageDisplay({ src, width, height, pixelWidth, pixelHeight }: {
|
| 15 |
src: string
|
|
@@ -18,9 +22,11 @@ export function ImageDisplay({ src, width, height, pixelWidth, pixelHeight }: {
|
|
| 18 |
pixelWidth: number
|
| 19 |
pixelHeight: number
|
| 20 |
}) {
|
|
|
|
|
|
|
| 21 |
return (
|
| 22 |
<Box flexDirection="column">
|
| 23 |
-
<InkPictureProvider>
|
| 24 |
<Image
|
| 25 |
src={src}
|
| 26 |
width={width}
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react'
|
| 2 |
import Image, { InkPictureProvider } from '../../ink-picture/index.js'
|
| 3 |
import { Box, Text } from '../../ink.js'
|
| 4 |
import { MessageResponse } from '../../components/MessageResponse.js'
|
| 5 |
import type { ImageShowOutput } from './ImageShowTool.js'
|
| 6 |
+
import { detectTerminalCaps } from './detectTerminal.js'
|
| 7 |
|
| 8 |
// ── Image display component ──
|
| 9 |
// Renders a placeholder in Ink's TUI and uses Kitty/Sixel protocol to draw
|
|
|
|
| 11 |
// the Ink render cycle). The placeholder reserves character cells so the TUI
|
| 12 |
// layout isn't broken; ink-picture's useDirectRenderer repositions the image
|
| 13 |
// after each Ink screen refresh.
|
| 14 |
+
//
|
| 15 |
+
// Terminal detection uses environment variables (TERM, TERM_PROGRAM, etc.)
|
| 16 |
+
// instead of ANSI escape queries, avoiding stdin conflicts with Ink's TUI.
|
| 17 |
|
| 18 |
export function ImageDisplay({ src, width, height, pixelWidth, pixelHeight }: {
|
| 19 |
src: string
|
|
|
|
| 22 |
pixelWidth: number
|
| 23 |
pixelHeight: number
|
| 24 |
}) {
|
| 25 |
+
const terminalInfo = useMemo(() => detectTerminalCaps(), [])
|
| 26 |
+
|
| 27 |
return (
|
| 28 |
<Box flexDirection="column">
|
| 29 |
+
<InkPictureProvider terminalInfo={terminalInfo}>
|
| 30 |
<Image
|
| 31 |
src={src}
|
| 32 |
width={width}
|
src/tools/ImageShowTool/detectTerminal.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { TerminalInfo } from '../../ink-picture/InkPictureProvider.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Detect terminal image protocol capabilities using environment variables only.
|
| 5 |
+
* This is the reliable path — no ANSI escape queries, no stdin hijacking.
|
| 6 |
+
*
|
| 7 |
+
* Based on the timg-era approach (commit baaa9e3) where detectImageProtocol()
|
| 8 |
+
* used TERM, TERM_PROGRAM, and KITTY_WINDOW_ID to pick the protocol.
|
| 9 |
+
*/
|
| 10 |
+
export function detectTerminalCaps(): Partial<TerminalInfo> {
|
| 11 |
+
const term = process.env.TERM ?? ''
|
| 12 |
+
const termProgram = process.env.TERM_PROGRAM ?? ''
|
| 13 |
+
|
| 14 |
+
const supportsUnicode = true // all modern terminals
|
| 15 |
+
const colorterm = process.env.COLORTERM ?? ''
|
| 16 |
+
const supportsColor =
|
| 17 |
+
colorterm === 'truecolor' ||
|
| 18 |
+
!!colorterm ||
|
| 19 |
+
term.includes('truecolor') ||
|
| 20 |
+
term.includes('256color')
|
| 21 |
+
|
| 22 |
+
// --- Kitty graphics ---
|
| 23 |
+
const supportsKittyGraphics =
|
| 24 |
+
termProgram === 'ghostty' ||
|
| 25 |
+
termProgram === 'kitty' ||
|
| 26 |
+
term.includes('kitty') ||
|
| 27 |
+
!!process.env.KITTY_WINDOW_ID
|
| 28 |
+
|
| 29 |
+
// --- Sixel graphics ---
|
| 30 |
+
const supportsSixelGraphics =
|
| 31 |
+
term.includes('sixel') ||
|
| 32 |
+
termProgram === 'ghostty' ||
|
| 33 |
+
termProgram === 'vscode' ||
|
| 34 |
+
(supportsKittyGraphics && termProgram === 'foot')
|
| 35 |
+
|
| 36 |
+
// --- iTerm2 inline images ---
|
| 37 |
+
let supportsITerm2Graphics = false
|
| 38 |
+
if (termProgram === 'iTerm.app') {
|
| 39 |
+
supportsITerm2Graphics = true
|
| 40 |
+
} else if (termProgram === 'WezTerm') {
|
| 41 |
+
// WezTerm supports iTerm2 inline from 20220319
|
| 42 |
+
const ver = process.env.TERM_PROGRAM_VERSION ?? ''
|
| 43 |
+
const date = parseInt(ver.split('-')[0], 10)
|
| 44 |
+
if (!Number.isNaN(date) && date >= 20220319) {
|
| 45 |
+
supportsITerm2Graphics = true
|
| 46 |
+
}
|
| 47 |
+
} else if (termProgram === 'WarpTerminal') {
|
| 48 |
+
// Warp supports iTerm2 inline from v0.2025.03.05.08.02
|
| 49 |
+
const ver = process.env.TERM_PROGRAM_VERSION ?? ''
|
| 50 |
+
const match = ver.match(/v?(\d+)\.(\d+)\.(\d+)/)
|
| 51 |
+
if (match) {
|
| 52 |
+
const [, year, month, day] = match.map(Number)
|
| 53 |
+
if (
|
| 54 |
+
year > 2025 ||
|
| 55 |
+
(year === 2025 && month > 3) ||
|
| 56 |
+
(year === 2025 && month === 3 && day >= 5)
|
| 57 |
+
) {
|
| 58 |
+
supportsITerm2Graphics = true
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
} else if (termProgram === 'vscode' && supportsSixelGraphics) {
|
| 62 |
+
// VS Code can do iTerm2 if it supports Sixel
|
| 63 |
+
supportsITerm2Graphics = true
|
| 64 |
+
} else if (!!process.env.KONSOLE_VERSION && supportsKittyGraphics) {
|
| 65 |
+
// Konsole uses Kitty; also supports iTerm2 from 22.04
|
| 66 |
+
const konsoleVer = parseInt(process.env.KONSOLE_VERSION, 10)
|
| 67 |
+
if (!Number.isNaN(konsoleVer) && konsoleVer >= 220400) {
|
| 68 |
+
supportsITerm2Graphics = true
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return {
|
| 73 |
+
supportsUnicode,
|
| 74 |
+
supportsColor,
|
| 75 |
+
supportsKittyGraphics,
|
| 76 |
+
supportsSixelGraphics,
|
| 77 |
+
supportsITerm2Graphics,
|
| 78 |
+
}
|
| 79 |
+
}
|