Skip to content

fix(tui-clipboard): skip native safety net on OSC52-capable terminals#20954

Merged
OutThisLife merged 5 commits into
mainfrom
fix/osc52-capable-terminals-skip-native-race
May 12, 2026
Merged

fix(tui-clipboard): skip native safety net on OSC52-capable terminals#20954
OutThisLife merged 5 commits into
mainfrom
fix/osc52-capable-terminals-skip-native-race

Conversation

@benbarclay

@benbarclay benbarclay commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm, Windows Terminal, VS Code), setClipboard() currently fires both OSC 52 AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland + wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy with empty stdin as an existence check, which is destructive (wipes clipboard to empty string), and the subsequent real wl-copy invocation races OSC 52 plus its own daemon's previous SIGTERM.

Symptoms reported: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH) had to press Ctrl+Shift+C three times before a selection landed in the clipboard. /copy with a selection showed "copied N characters" but left the clipboard empty. Adding env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which short-circuits copyNative() via the DISPLAY-absent early-return at osc.ts:285) made every copy work instantly — proving OSC 52 alone is sufficient on Ghostty and that copyNative() is actively destructive there.

This PR adds an OSC52_CAPABLE_TERMINALS allowlist to utils/env.ts (the constant + supportsOsc52Clipboard() helper live there to avoid a circular import — ink/terminal.ts already imports from ink/termio/osc.ts, so the allowlist can't live in terminal.ts), and gates copyNative() on the terminal NOT being on it. The "native safety net" continues to fire on unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app, etc.) where OSC 52 is less reliable.

Changes

File Change
ui-tui/packages/hermes-ink/src/utils/env.ts Add OSC52_CAPABLE_TERMINALS constant + supportsOsc52Clipboard() export. Lives in utils/env.ts rather than ink/terminal.ts to avoid a circular import (ink/terminal.tsink/termio/osc.ts → would-be ink/terminal.ts); pattern still mirrors EXTENDED_KEYS_TERMINALS / supportsExtendedKeys(). Helper accepts an optional terminal-name argument (default = env.terminal) for testability.
ui-tui/packages/hermes-ink/src/ink/termio/osc.ts In setClipboard(), gate copyNative(text) on !supportsOsc52Clipboard(); update the existing safety-net comment block to document the skip condition and the Ghostty+Wayland symptom it fixes.
ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts Add 16 new tests covering supportsOsc52Clipboard: allowlisted terminals return true, non-allowlisted return false, null returns false, default-argument call returns a boolean.

Why this allowlist specifically

Terminal OSC 52 status Verdict
ghostty Native OSC 52 clipboard writes on by default (clipboard-write = allow) ✅ safe to skip native
kitty OSC 52 native and the protocol's reference implementation
WezTerm OSC 52 on by default
windows-terminal OSC 52 on by default
vscode xterm.js supports OSC 52 with a permission prompt (fine — user consent is how it should work)
iTerm.app OSC 52 disabled by default (see existing comment at osc.ts:165) ❌ keep current behaviour
Alacritty / GNOME Terminal / Konsole / xterm / Terminal.app Unknown or no OSC 52 ❌ keep current behaviour
tmux Uses its own load-buffer path; already special-cased in setClipboard() ❌ keep current behaviour

Terminal-name values match what detectTerminal() in utils/env.ts returns: TERM=xterm-ghostty'ghostty', TERM containing kitty'kitty', WT_SESSION set → 'windows-terminal', TERM_PROGRAMWezTerm / vscode as-is.

Related issues / prior art

Validation

Case Before After
Ghostty + Wayland + wl-copy, /copy with selection "copied N characters" toast, clipboard empty ~30% of the time (daemon race) "copied N characters", clipboard has text every time
Ghostty, Ctrl+Shift+C with selection Works on 3rd attempt Works on 1st attempt
Ghostty, first-ever copy of a session (no cached probe) Destructive wl-copy [] probe wipes clipboard Probe never runs; OSC 52 emits cleanly
GNOME Terminal / Terminal.app / unknown terminal Unchanged (native fallback fires as before) Unchanged
SSH (any terminal) Unchanged (!SSH_CONNECTION gate already short-circuits native) Unchanged
tmux (any terminal) Unchanged Unchanged

Tests:

  • ./node_modules/.bin/vitest run packages/hermes-ink/src/ink/termio/osc.test.ts21 passed (5 existing + 16 new), 0 failed.
  • ./node_modules/.bin/vitest run — 491 passed / 0 test-level failures. 15 test files fail to load due to a pre-existing issue importing packages/hermes-ink/index.js./dist/entry-exports.js (requires a prior npm run build); confirmed identical failure count on main before my changes (main: 475 passed, 15 import-error files; PR: 491 passed, same 15 import-error files).
  • ./node_modules/.bin/tsc --noEmit -p tsconfig.json — clean (exit 0).
  • ./node_modules/.bin/eslint on touched files — clean.
  • ./node_modules/.bin/prettier --check on touched files — clean.

Follow-ups (not in this PR, intentionally)

  1. Non-destructive probe. probeLinuxCopy() currently runs wl-copy [] / xclip -selection clipboard / xsel --clipboard --input with empty stdin to check if the binary exists. These are destructive. A follow-up PR should switch to which-style detection or --version probes. This PR leaves that code untouched and simply avoids calling it on the terminals that don't need native at all.

  2. Misleading /copy message on no-selection path. /copy with no selection currently emits "sent OSC52 copy sequence (terminal support required)". Existing open PR Feat/dashboard chat #13379 fixes this incidentally as part of dashboard-chat work; could also be pulled out standalone. Not addressed here.

  3. Allowlist expansion. Alacritty, GNOME Terminal (via recent VTE OSC 52 work), etc. could join the allowlist after verification. Intentionally conservative in this first pass.

Checklist

  • Commit message follows Conventional Commits (fix(tui-clipboard): prefix)
  • Single-scope PR (one allowlist addition, no unrelated changes)
  • vitest run passes for touched test file (21/21)
  • tsc --noEmit clean for touched files
  • Cross-platform: no new OS-specific code; only narrows an existing always-on code path
  • Backward compatible: default behaviour on non-allowlisted terminals unchanged

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm,
Windows Terminal, VS Code), setClipboard() currently fires both OSC 52
AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland
+ wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy
with empty stdin as an existence check (destructive — wipes clipboard
to empty string), and the subsequent real wl-copy invocation races
OSC 52 plus its own daemon's previous SIGTERM.

Symptom: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH)
had to press Ctrl+Shift+C three times before a selection landed.
env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which
short-circuits copyNative via the DISPLAY-absent early-return) made
every copy work instantly — proving OSC 52 alone is sufficient on
Ghostty and that copyNative() is actively destructive there.

Add OSC52_CAPABLE_TERMINALS allowlist to terminal.ts (same pattern as
the existing EXTENDED_KEYS_TERMINALS), and gate copyNative() on the
terminal NOT being on it. The native safety net continues to fire on
unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app,
etc.) where OSC 52 is less reliable.
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/osc52-capable-terminals-skip-native-race vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8175 on HEAD, 8175 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4307 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists labels May 7, 2026
@benbarclay benbarclay requested a review from OutThisLife May 11, 2026 23:53
@OutThisLife OutThisLife requested a review from Copilot May 12, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve TUI clipboard reliability on terminals with strong OSC 52 support by skipping the parallel native clipboard “safety net” (wl-copy/xclip/pbcopy) to avoid destructive probes and race conditions (notably on Wayland + wl-copy).

Changes:

  • Add an OSC52-capable terminal allowlist and a supportsOsc52Clipboard() helper in terminal.ts.
  • Gate copyNative(text) in setClipboard() behind !supportsOsc52Clipboard() and expand the safety-net commentary.
  • Add unit tests for the supportsOsc52Clipboard() allowlist helper.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Skips native clipboard fallback on allowlisted terminals to prevent wl-copy/probe races.
ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts Adds tests for the OSC52-capable allowlist helper.
ui-tui/packages/hermes-ink/src/ink/terminal.ts Introduces OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() export.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Outdated
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Outdated
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts
- Move OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() from
  ink/terminal.ts to utils/env.ts. ink/terminal.ts already imports
  link from ink/termio/osc.ts; importing back into termio/osc.ts
  introduced a circular dependency. utils/env.ts has no deps on
  either file and already owns terminal detection (detectTerminal()),
  so the helper sits naturally next to it.

- Replace the inline gating (!SSH_CONNECTION && !supportsOsc52Clipboard())
  with a pure shouldUseNativeClipboard(env, terminal) helper. The old
  expression skipped native on allowlisted terminals even when
  setClipboard() wouldn't actually emit OSC 52 (e.g. inside
  TMUX/STY where we use tmux load-buffer instead, or when the user
  has set HERMES_TUI_FORCE_OSC52=0). That made the clipboard write
  a no-op in those configurations. The new helper:
    1. SSH_CONNECTION set -> false (existing behaviour)
    2. TMUX or STY set -> true (we go through load-buffer, no race)
    3. shouldEmitClipboardSequence() false -> true (native is the
       only path left when OSC 52 is suppressed)
    4. Otherwise: skip native iff terminal is allowlisted.

- Add 11 tests for shouldUseNativeClipboard covering the SSH guard,
  TMUX/STY tmux-inside-Ghostty case, HERMES_TUI_FORCE_OSC52=0
  override, allowlisted vs non-allowlisted terminals, precedence,
  and default-args smoke. Tests follow the package's existing
  parameterised-helper style (no vi.mock; helpers accept env and
  terminal as arguments).

- Update test imports to the new utils/env.js path.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread ui-tui/packages/hermes-ink/src/utils/env.ts
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.ts
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts Outdated
@benbarclay

Copy link
Copy Markdown
Collaborator Author

@copilot review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.test.ts Outdated
Comment thread ui-tui/packages/hermes-ink/src/utils/env.ts Outdated
@benbarclay

Copy link
Copy Markdown
Collaborator Author

@copilot review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Outdated
Comment thread ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Outdated
@benbarclay

Copy link
Copy Markdown
Collaborator Author

@copilot review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@OutThisLife OutThisLife merged commit 3c23b15 into main May 12, 2026
13 of 16 checks passed
@OutThisLife OutThisLife deleted the fix/osc52-capable-terminals-skip-native-race branch May 12, 2026 02:40
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…NousResearch#20954)

* fix(tui-clipboard): skip native safety net on OSC52-capable terminals

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm,
Windows Terminal, VS Code), setClipboard() currently fires both OSC 52
AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland
+ wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy
with empty stdin as an existence check (destructive — wipes clipboard
to empty string), and the subsequent real wl-copy invocation races
OSC 52 plus its own daemon's previous SIGTERM.

Symptom: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH)
had to press Ctrl+Shift+C three times before a selection landed.
env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which
short-circuits copyNative via the DISPLAY-absent early-return) made
every copy work instantly — proving OSC 52 alone is sufficient on
Ghostty and that copyNative() is actively destructive there.

Add OSC52_CAPABLE_TERMINALS allowlist to terminal.ts (same pattern as
the existing EXTENDED_KEYS_TERMINALS), and gate copyNative() on the
terminal NOT being on it. The native safety net continues to fire on
unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app,
etc.) where OSC 52 is less reliable.

* fix(tui-clipboard): address Copilot review feedback

- Move OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() from
  ink/terminal.ts to utils/env.ts. ink/terminal.ts already imports
  link from ink/termio/osc.ts; importing back into termio/osc.ts
  introduced a circular dependency. utils/env.ts has no deps on
  either file and already owns terminal detection (detectTerminal()),
  so the helper sits naturally next to it.

- Replace the inline gating (!SSH_CONNECTION && !supportsOsc52Clipboard())
  with a pure shouldUseNativeClipboard(env, terminal) helper. The old
  expression skipped native on allowlisted terminals even when
  setClipboard() wouldn't actually emit OSC 52 (e.g. inside
  TMUX/STY where we use tmux load-buffer instead, or when the user
  has set HERMES_TUI_FORCE_OSC52=0). That made the clipboard write
  a no-op in those configurations. The new helper:
    1. SSH_CONNECTION set -> false (existing behaviour)
    2. TMUX or STY set -> true (we go through load-buffer, no race)
    3. shouldEmitClipboardSequence() false -> true (native is the
       only path left when OSC 52 is suppressed)
    4. Otherwise: skip native iff terminal is allowlisted.

- Add 11 tests for shouldUseNativeClipboard covering the SSH guard,
  TMUX/STY tmux-inside-Ghostty case, HERMES_TUI_FORCE_OSC52=0
  override, allowlisted vs non-allowlisted terminals, precedence,
  and default-args smoke. Tests follow the package's existing
  parameterised-helper style (no vi.mock; helpers accept env and
  terminal as arguments).

- Update test imports to the new utils/env.js path.

* fix(tui-clipboard): address Copilot round 2 feedback

* fix(tui-clipboard): address Copilot round 3 feedback

* fix(tui-clipboard): address Copilot round 4 feedback
jsboige pushed a commit to jsboige/hermes-agent that referenced this pull request May 14, 2026
…NousResearch#20954)

* fix(tui-clipboard): skip native safety net on OSC52-capable terminals

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm,
Windows Terminal, VS Code), setClipboard() currently fires both OSC 52
AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland
+ wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy
with empty stdin as an existence check (destructive — wipes clipboard
to empty string), and the subsequent real wl-copy invocation races
OSC 52 plus its own daemon's previous SIGTERM.

Symptom: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH)
had to press Ctrl+Shift+C three times before a selection landed.
env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which
short-circuits copyNative via the DISPLAY-absent early-return) made
every copy work instantly — proving OSC 52 alone is sufficient on
Ghostty and that copyNative() is actively destructive there.

Add OSC52_CAPABLE_TERMINALS allowlist to terminal.ts (same pattern as
the existing EXTENDED_KEYS_TERMINALS), and gate copyNative() on the
terminal NOT being on it. The native safety net continues to fire on
unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app,
etc.) where OSC 52 is less reliable.

* fix(tui-clipboard): address Copilot review feedback

- Move OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() from
  ink/terminal.ts to utils/env.ts. ink/terminal.ts already imports
  link from ink/termio/osc.ts; importing back into termio/osc.ts
  introduced a circular dependency. utils/env.ts has no deps on
  either file and already owns terminal detection (detectTerminal()),
  so the helper sits naturally next to it.

- Replace the inline gating (!SSH_CONNECTION && !supportsOsc52Clipboard())
  with a pure shouldUseNativeClipboard(env, terminal) helper. The old
  expression skipped native on allowlisted terminals even when
  setClipboard() wouldn't actually emit OSC 52 (e.g. inside
  TMUX/STY where we use tmux load-buffer instead, or when the user
  has set HERMES_TUI_FORCE_OSC52=0). That made the clipboard write
  a no-op in those configurations. The new helper:
    1. SSH_CONNECTION set -> false (existing behaviour)
    2. TMUX or STY set -> true (we go through load-buffer, no race)
    3. shouldEmitClipboardSequence() false -> true (native is the
       only path left when OSC 52 is suppressed)
    4. Otherwise: skip native iff terminal is allowlisted.

- Add 11 tests for shouldUseNativeClipboard covering the SSH guard,
  TMUX/STY tmux-inside-Ghostty case, HERMES_TUI_FORCE_OSC52=0
  override, allowlisted vs non-allowlisted terminals, precedence,
  and default-args smoke. Tests follow the package's existing
  parameterised-helper style (no vi.mock; helpers accept env and
  terminal as arguments).

- Update test imports to the new utils/env.js path.

* fix(tui-clipboard): address Copilot round 2 feedback

* fix(tui-clipboard): address Copilot round 3 feedback

* fix(tui-clipboard): address Copilot round 4 feedback
AlexFoxD pushed a commit to AlexFoxD/hermes-agent that referenced this pull request May 21, 2026
…NousResearch#20954)

* fix(tui-clipboard): skip native safety net on OSC52-capable terminals

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm,
Windows Terminal, VS Code), setClipboard() currently fires both OSC 52
AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland
+ wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy
with empty stdin as an existence check (destructive — wipes clipboard
to empty string), and the subsequent real wl-copy invocation races
OSC 52 plus its own daemon's previous SIGTERM.

Symptom: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH)
had to press Ctrl+Shift+C three times before a selection landed.
env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which
short-circuits copyNative via the DISPLAY-absent early-return) made
every copy work instantly — proving OSC 52 alone is sufficient on
Ghostty and that copyNative() is actively destructive there.

Add OSC52_CAPABLE_TERMINALS allowlist to terminal.ts (same pattern as
the existing EXTENDED_KEYS_TERMINALS), and gate copyNative() on the
terminal NOT being on it. The native safety net continues to fire on
unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app,
etc.) where OSC 52 is less reliable.

* fix(tui-clipboard): address Copilot review feedback

- Move OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() from
  ink/terminal.ts to utils/env.ts. ink/terminal.ts already imports
  link from ink/termio/osc.ts; importing back into termio/osc.ts
  introduced a circular dependency. utils/env.ts has no deps on
  either file and already owns terminal detection (detectTerminal()),
  so the helper sits naturally next to it.

- Replace the inline gating (!SSH_CONNECTION && !supportsOsc52Clipboard())
  with a pure shouldUseNativeClipboard(env, terminal) helper. The old
  expression skipped native on allowlisted terminals even when
  setClipboard() wouldn't actually emit OSC 52 (e.g. inside
  TMUX/STY where we use tmux load-buffer instead, or when the user
  has set HERMES_TUI_FORCE_OSC52=0). That made the clipboard write
  a no-op in those configurations. The new helper:
    1. SSH_CONNECTION set -> false (existing behaviour)
    2. TMUX or STY set -> true (we go through load-buffer, no race)
    3. shouldEmitClipboardSequence() false -> true (native is the
       only path left when OSC 52 is suppressed)
    4. Otherwise: skip native iff terminal is allowlisted.

- Add 11 tests for shouldUseNativeClipboard covering the SSH guard,
  TMUX/STY tmux-inside-Ghostty case, HERMES_TUI_FORCE_OSC52=0
  override, allowlisted vs non-allowlisted terminals, precedence,
  and default-args smoke. Tests follow the package's existing
  parameterised-helper style (no vi.mock; helpers accept env and
  terminal as arguments).

- Update test imports to the new utils/env.js path.

* fix(tui-clipboard): address Copilot round 2 feedback

* fix(tui-clipboard): address Copilot round 3 feedback

* fix(tui-clipboard): address Copilot round 4 feedback
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…NousResearch#20954)

* fix(tui-clipboard): skip native safety net on OSC52-capable terminals

On terminals with first-class OSC 52 support (Ghostty, kitty, WezTerm,
Windows Terminal, VS Code), setClipboard() currently fires both OSC 52
AND a parallel native-tool write (wl-copy / xclip / pbcopy). On Wayland
+ wl-copy this corrupts the clipboard: probeLinuxCopy() runs wl-copy
with empty stdin as an existence check (destructive — wipes clipboard
to empty string), and the subsequent real wl-copy invocation races
OSC 52 plus its own daemon's previous SIGTERM.

Symptom: user on Arch + Ghostty + wl-copy (Wayland, no tmux, no SSH)
had to press Ctrl+Shift+C three times before a selection landed.
env -u WAYLAND_DISPLAY -u DISPLAY HERMES_TUI_FORCE_OSC52=1 (which
short-circuits copyNative via the DISPLAY-absent early-return) made
every copy work instantly — proving OSC 52 alone is sufficient on
Ghostty and that copyNative() is actively destructive there.

Add OSC52_CAPABLE_TERMINALS allowlist to terminal.ts (same pattern as
the existing EXTENDED_KEYS_TERMINALS), and gate copyNative() on the
terminal NOT being on it. The native safety net continues to fire on
unrecognised terminals (xterm, GNOME Terminal, Konsole, Terminal.app,
etc.) where OSC 52 is less reliable.

* fix(tui-clipboard): address Copilot review feedback

- Move OSC52_CAPABLE_TERMINALS + supportsOsc52Clipboard() from
  ink/terminal.ts to utils/env.ts. ink/terminal.ts already imports
  link from ink/termio/osc.ts; importing back into termio/osc.ts
  introduced a circular dependency. utils/env.ts has no deps on
  either file and already owns terminal detection (detectTerminal()),
  so the helper sits naturally next to it.

- Replace the inline gating (!SSH_CONNECTION && !supportsOsc52Clipboard())
  with a pure shouldUseNativeClipboard(env, terminal) helper. The old
  expression skipped native on allowlisted terminals even when
  setClipboard() wouldn't actually emit OSC 52 (e.g. inside
  TMUX/STY where we use tmux load-buffer instead, or when the user
  has set HERMES_TUI_FORCE_OSC52=0). That made the clipboard write
  a no-op in those configurations. The new helper:
    1. SSH_CONNECTION set -> false (existing behaviour)
    2. TMUX or STY set -> true (we go through load-buffer, no race)
    3. shouldEmitClipboardSequence() false -> true (native is the
       only path left when OSC 52 is suppressed)
    4. Otherwise: skip native iff terminal is allowlisted.

- Add 11 tests for shouldUseNativeClipboard covering the SSH guard,
  TMUX/STY tmux-inside-Ghostty case, HERMES_TUI_FORCE_OSC52=0
  override, allowlisted vs non-allowlisted terminals, precedence,
  and default-args smoke. Tests follow the package's existing
  parameterised-helper style (no vi.mock; helpers accept env and
  terminal as arguments).

- Update test imports to the new utils/env.js path.

* fix(tui-clipboard): address Copilot round 2 feedback

* fix(tui-clipboard): address Copilot round 3 feedback

* fix(tui-clipboard): address Copilot round 4 feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants