diff --git a/ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts b/ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts new file mode 100644 index 000000000..9869189ed --- /dev/null +++ b/ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import { shouldUseAnsiDim } from './Text.js' + +describe('shouldUseAnsiDim', () => { + it('disables ANSI dim on VTE terminals by default', () => { + expect(shouldUseAnsiDim({ VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('keeps ANSI dim enabled elsewhere by default', () => { + expect(shouldUseAnsiDim({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true) + }) + + it('honors explicit env override', () => { + expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(true) + expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '0' } as NodeJS.ProcessEnv)).toBe(false) + }) +}) diff --git a/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx b/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx index 9459b78a2..d6b7fdccd 100644 --- a/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx @@ -3,6 +3,9 @@ import React from 'react' import { c as _c } from 'react/compiler-runtime' import type { Color, Styles } from '../styles.js' + +const ENV_ON_RE = /^(?:1|true|yes|on)$/i +const ENV_OFF_RE = /^(?:0|false|no|off)$/i type BaseProps = { /** * Change text color. Accepts a raw color value (rgb, hex, ansi). @@ -62,6 +65,20 @@ type WeightProps = } export type Props = BaseProps & WeightProps +export function shouldUseAnsiDim(env: NodeJS.ProcessEnv = process.env): boolean { + const override = (env.HERMES_TUI_DIM ?? '').trim() + + if (ENV_ON_RE.test(override)) { + return true + } + + if (ENV_OFF_RE.test(override)) { + return false + } + + return !env.VTE_VERSION +} + const memoizedStylesForWrap: Record, Styles> = { wrap: { flexGrow: 0, @@ -143,6 +160,7 @@ export default function Text(t0: Props) { const strikethrough = t3 === undefined ? false : t3 const inverse = t4 === undefined ? false : t4 const wrap = t5 === undefined ? 'wrap' : t5 + const effectiveDim = dim && shouldUseAnsiDim() if (children === undefined || children === null) { return null @@ -174,11 +192,11 @@ export default function Text(t0: Props) { let t8 - if ($[4] !== dim) { - t8 = dim && { - dim + if ($[4] !== effectiveDim) { + t8 = effectiveDim && { + dim: effectiveDim } - $[4] = dim + $[4] = effectiveDim $[5] = t8 } else { t8 = $[5] diff --git a/ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts b/ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts new file mode 100644 index 000000000..02ea9ebd2 --- /dev/null +++ b/ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' + +import { shouldEmitClipboardSequence } from './osc.js' + +describe('shouldEmitClipboardSequence', () => { + it('suppresses local multiplexer clipboard OSC by default', () => { + expect(shouldEmitClipboardSequence({ TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)).toBe(false) + expect(shouldEmitClipboardSequence({ STY: '1234.pts-0.host' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('keeps OSC enabled for remote or plain local terminals', () => { + expect(shouldEmitClipboardSequence({ SSH_CONNECTION: '1', TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)).toBe( + true + ) + expect(shouldEmitClipboardSequence({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true) + }) + + it('honors explicit env override', () => { + expect(shouldEmitClipboardSequence({ HERMES_TUI_CLIPBOARD_OSC52: '1', TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)).toBe( + true + ) + expect(shouldEmitClipboardSequence({ HERMES_TUI_COPY_OSC52: '0', TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe( + false + ) + }) +}) diff --git a/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts b/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts index 49f222395..3230767e7 100644 --- a/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts +++ b/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts @@ -11,6 +11,8 @@ import { BEL, ESC, ESC_TYPE, SEP } from './ansi.js' import type { Action, Color, TabStatusAction } from './types.js' export const OSC_PREFIX = ESC + String.fromCharCode(ESC_TYPE.OSC) +const ENV_ON_RE = /^(?:1|true|yes|on)$/i +const ENV_OFF_RE = /^(?:0|false|no|off)$/i /** String Terminator (ESC \) - alternative to BEL for terminating OSC */ export const ST = ESC + '\\' @@ -81,6 +83,20 @@ export function getClipboardPath(): ClipboardPath { return 'osc52' } +export function shouldEmitClipboardSequence(env: NodeJS.ProcessEnv = process.env): boolean { + const override = (env.HERMES_TUI_CLIPBOARD_OSC52 ?? env.HERMES_TUI_COPY_OSC52 ?? '').trim() + + if (ENV_ON_RE.test(override)) { + return true + } + + if (ENV_OFF_RE.test(override)) { + return false + } + + return !!env['SSH_CONNECTION'] || (!env['TMUX'] && !env['STY']) +} + /** * Wrap a payload in tmux's DCS passthrough: ESC P tmux ; ESC \ * tmux forwards the payload to the outer terminal, bypassing its own parser. @@ -152,6 +168,7 @@ export async function tmuxLoadBuffer(text: string): Promise { export async function setClipboard(text: string): Promise { const b64 = Buffer.from(text, 'utf8').toString('base64') const raw = osc(OSC.CLIPBOARD, 'c', b64) + const emitSequence = shouldEmitClipboardSequence(process.env) // Native safety net — fire FIRST, before the tmux await, so a quick // focus-switch after selecting doesn't race pbcopy. Previously this ran @@ -170,10 +187,10 @@ export async function setClipboard(text: string): Promise { // Inner OSC uses BEL directly (not osc()) — ST's ESC would need doubling // too, and BEL works everywhere for OSC 52. if (tmuxBufferLoaded) { - return tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`) + return emitSequence ? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`) : '' } - return raw + return emitSequence ? raw : '' } // Linux clipboard tool: undefined = not yet probed, null = none available.