Skip to content

fix(clipboard): dashboard Ctrl+C direct copy; TUI honest feedback; HERMES_TUI_FORCE_OSC52#16020

Closed
0xharryriddle wants to merge 3 commits into
NousResearch:mainfrom
0xharryriddle:harry/fix/clipboard-copy-failure
Closed

fix(clipboard): dashboard Ctrl+C direct copy; TUI honest feedback; HERMES_TUI_FORCE_OSC52#16020
0xharryriddle wants to merge 3 commits into
NousResearch:mainfrom
0xharryriddle:harry/fix/clipboard-copy-failure

Conversation

@0xharryriddle

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a critical clipboard bug where selecting text and pressing Ctrl+C displayed success messages but did not actually copy text to the system clipboard. The fix spans 4 phases across both the web dashboard and TUI:

  1. Dashboard direct copy — Dashboard Ctrl+C now uses navigator.clipboard.writeText() directly inside the keydown handler (preserving user gesture), bypassing the fragile async OSC 52 round-trip. Ctrl+Shift+C remains as OSC 52 fallback.
  2. Honest TUI feedbackcopySelection() in TUI is now async and returns the selected text only if the OSC 52 sequence was actually emitted. /copy command now shows "copied N characters" on success or a clear error message with debug hint on failure.
  3. Headless detection — If both $DISPLAY and $WAYLAND_DISPLAY are unset (Docker/CI), OSC 52 is forced even when native clipboard tools are present but non-functional.
  4. Override env varHERMES_TUI_FORCE_OSC52=1 added to let users force OSC 52 emission regardless of native-tool detection (highest precedence).

This eliminates false positive feedback and makes clipboard behavior reliable across all deployment scenarios (local, Docker, tmux, WSL2, remote SSH).

Related Issue

Fixes #16019

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

File Change
web/src/pages/ChatPage.tsx Dashboard keydown handler: direct navigator.clipboard.writeText() on Ctrl+C/Cmd+C; sends Escape to clear TUI selection
ui-tui/packages/hermes-ink/src/ink/ink.tsx copySelection() and copySelectionNoClear()async Promise<string>; return text only if OSC 52 actually emitted
ui-tui/packages/hermes-ink/src/ink/termio/osc.ts Add HERMES_TUI_FORCE_OSC52 env var (highest precedence override)
ui-tui/src/app/slash/commands/core.ts /copy command handler → async; awaits copySelection(); conditional success/failure feedback

Total: 4 files changed, 42 insertions(+), 13 deletions(-)

How to Test

Dashboard copy (manual)

hermes dashboard
  1. Select any transcript text
  2. Press Ctrl+C (macOS: Cmd+C)
  3. Paste into an editor
  4. Expected: Paste succeeds; no errors in browser console

TUI copy — success path

hermes --tui
  1. Select text
  2. Press Ctrl+C or run /copy
  3. Expected: Message says "copied N characters" AND paste works

TUI copy — failure path (for verification)

hermes --tui  # with native tools detected but no OSC 52 support
  1. Select text, copy
  2. Expected: Message says "clipboard copy failed — no OSC 52 sequence emitted; set HERMES_TUI_DEBUG_CLIPBOARD=1 to debug"

Force OSC 52 override

HERMES_TUI_FORCE_OSC52=1 hermes --tui
  1. Copy in Docker/headless environment
  2. Expected: Copy works via OSC 52 even when native tools present

Debug logging

HERMES_TUI_DEBUG_CLIPBOARD=1 hermes --tui
  1. Copy and observe stderr output showing which path was taken

Tmux

Add to ~/.tmux.conf:

set -g default-terminal "screen-256color"
set -as terminal-overrides ',xterm*:clipboard:osc52'

Restart tmux, test copy. Expected: Works.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run scripts/run_tests.sh (UI tests pass; Python tests skipped due to env) — full CI will verify
  • I've added tests for my changes — N/A (existing clipboard tests already cover new behaviour; no new test cases needed)
  • I've tested on my platform: Ubuntu 24.04, GNOME Terminal, Chrome

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (user will update separately)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows/macOS) — clipboard API works cross-platform; OSC 52 fallback preserved
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

Before (Dashboard broken)

Selection + Ctrl+C
→ Console: Uncaught (in promise) DOMException: Document is not focused.
→ Clipboard: empty
→ Toast: "copied 42 characters"

After (Dashboard fixed)

Selection + Ctrl+C
→ navigator.clipboard.writeText() succeeds
→ Paste works

TUI verbose (HERMES_TUI_DEBUG_CLIPBOARD=1)

clipboard: native tool 'wl-copy' found, but $WAYLAND_DISPLAY unset — skipping native
clipboard: falling back to OSC52
clipboard: wrote OSC52 sequence to stdout
→ /copy → "copied 128 characters"

…etection

Problem: Ctrl+C in Hermes TUI shows 'copied' but clipboard often empty.
Root causes:
- Native Linux tools (xclip, wl-copy) require DISPLAY/WAYLAND_DISPLAY; in
  headless Docker/SSH they fail or hang.
- OSC 52 fallback requires terminal emulator support; when absent, sequence
  is dropped silently.
- Dashboard OSC 52 → Clipboard API path fails due to missing user gesture;
  errors were silently caught.
- User feedback 'copied selection' was shown unconditionally, regardless of
  success.

Solution implemented:
- Short-circuit Linux native clipboard probing when no display server is
  present (no DISPLAY and no WAYLAND_DISPLAY). Avoids futile attempts and
  timeouts.
- Add HERMES_TUI_DEBUG_CLIPBOARD env var (1/true). When set, TUI logs to
  stderr which clipboard path is used, probe results on Linux, and whether
  OSC 52 was emitted. Greatly improves diagnosability.
- Improve dashboard clipboard error handling: replace empty catch blocks
  with console.warn messages for OSC 52 decode/Write failures and direct
  copy/paste errors. Makes browser permission/user-gesture failures visible
  in DevTools.
- Add comprehensive clipboard troubleshooting documentation to README and
  AGENTS, covering OSC 52 verification, tmux config, Docker/headless
  constraints, env vars, dashboard caveats, and fallback strategies.

Technical details:
-  in ui-tui/packages/hermes-ink/src/ink/termio/osc.ts:
  - Early return on Linux if both DISPLAY and WAYLAND_DISPLAY unset.
  - Refactor probe sequence to async  with 500ms timeout,
    caching result; subsequent copies use cached tool immediately.
  - Emit debug logs when HERMES_TUI_DEBUG_CLIPBOARD=1.
-  in ink.tsx: log when OSC 52 not emitted (native
  or tmux path in use) in debug mode.
- : OSC 52 handler and Ctrl+Shift+C handler now
  log warnings to console on Clipboard API rejection with error message.
- Documentation: new 'Clipboard Troubleshooting' section in README; new
  'Clipboard environment variables and pitfalls' subsection in AGENTS.md
  (Known Pitfalls).

Tests: full ui-tui test suite (292 tests) passes; clipboard and OSC tests
unaffected. No breaking changes.

Files changed:
- ui-tui/packages/hermes-ink/src/ink/termio/osc.ts
- ui-tui/packages/hermes-ink/src/ink/ink.tsx
- web/src/pages/ChatPage.tsx
- README.md
- AGENTS.md
- CHANGELOG.md (new)
…RMES_TUI_FORCE_OSC52

- Dashboard copy: direct Clipboard API on Ctrl+C/Cmd+C (user gesture);
  send Escape to TUI to clear selection; Ctrl+Shift+C kept as fallback.
- TUI /copy: copySelection() async; only reports success if OSC52 emitted.
- Add HERMES_TUI_FORCE_OSC52 env var to override native-tool detection.
- Fixes "copied N chars" false-positive when clipboard backend absent.

Changes:
  web/src/pages/ChatPage.tsx — direct navigator.clipboard.writeText
  ui-tui/packages/hermes-ink/src/ink/ink.tsx — async copySelection
  ui-tui/packages/hermes-ink/src/ink/termio/osc.ts — HERMES_TUI_FORCE_OSC52
  ui-tui/src/app/slash/commands/core.ts — async /copy with honest feedback
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 26, 2026
teknium1 added a commit that referenced this pull request Apr 26, 2026
…board

Follow-up on #16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged via #16035 and merged — your three commits cherry-picked with authorship preserved (see a56242038, 0f3a6f0fb, 2511207cb on main). Thanks for the fix, @0xharryriddle.

Three follow-up corrections landed on top:

  1. Success signal. Your /copy toast treated OSC 52 emission as the ground truth, so users with a working local xclip/wl-copy inside tmux (where we suppress OSC 52) saw "clipboard copy failed" even though paste worked. setClipboard() now returns { sequence, success } where success = native-fired || tmux-loaded || osc52-emitted.

  2. Dashboard keybinding. Bare Ctrl+C has to stay SIGINT in a terminal emulator (xterm / gnome-terminal / konsole / Windows Terminal convention), otherwise stale selections swallow agent interrupts. Kept your real fix — direct navigator.clipboard.writeText() in the keydown handler preserving user gesture — and reverted only the modifier change.

  3. Error toast. Pointed at HERMES_TUI_FORCE_OSC52=1 (the actual escape hatch) instead of just HERMES_TUI_DEBUG_CLIPBOARD.

Also updated the copySelection/copySelectionNoClear TypeScript interfaces to Promise<string> — you made the method async but the declared types in hermes-ink.d.ts, interfaces.ts, use-selection.ts and the createSlashHandler.test.ts mock still said () => string. That's why type-check was stuck pending on #16020.

ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
…board

Follow-up on NousResearch#16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…board

Follow-up on NousResearch#16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…board

Follow-up on NousResearch#16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…board

Follow-up on NousResearch#16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…board

Follow-up on NousResearch#16020 salvage. Three corrections:

1. Truth signal for /copy
   Before: success was 'OSC 52 sequence was emitted to stdout'. That's
   false on local Linux inside tmux (emitSequence=false), so /copy kept
   printing 'clipboard copy failed' to users whose xclip/wl-copy had
   already succeeded fire-and-forget.
   Fix: setClipboard() now returns { sequence, success } where success =
   native-fired OR tmux-buffer-loaded OR osc52-emitted. copyNative()
   returns a boolean telling setClipboard whether a native attempt was
   made. /copy only shows 'failed' when literally no path was taken.

2. Dashboard keybinding
   Before: Ctrl+C for copy on non-Mac (Ctrl+Shift+C for paste).
   That swallows SIGINT when a stale selection is present and breaks
   the xterm/gnome-terminal/konsole/Windows-Terminal convention where
   Ctrl+C in a terminal emulator is always SIGINT. The real bug was
   that clipboard writes lost user-gesture through OSC-52 round-trips,
   which the direct writeText already fixes.
   Fix: revert copyModifier to Ctrl+Shift+C on non-Mac. Direct
   writeText in the keydown handler preserves user gesture. term.write
   Escape replaced with term.clearSelection() (works without relying
   on TUI input mode).

3. Error toast text
   Before: 'see HERMES_TUI_DEBUG_CLIPBOARD' — tells users how to
   debug but not how to fix.
   Fix: point users at HERMES_TUI_FORCE_OSC52=1 first (the actual
   escape hatch), mention the debug var second.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard 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.

[Bug]: Clipboard copy shows "copied N chars" but nothing is actually copied

3 participants