chenbhao Claude Big Pickle commited on
Commit
fbafa72
·
1 Parent(s): 999003d

fix: use env-var terminal detection to avoid stdin conflicts in TUI

Browse files

Replace 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
+ }