Terminal: smooth copy-paste for remote users (PTY/WebSocket) #69

Closed
opened 2026-04-29 11:14:02 +00:00 by mahmoud · 5 comments
Owner

Problem

Copy-paste between a remote user and the hero_router terminal (PTY over
WebSocket) is not working smoothly. This was a blocker when using tmux
remotely and is a hard requirement for replacing TeamWorks.

Requirements

  • Copy-paste must work reliably between the user's local browser and
    the remote terminal session
  • Must not be heavy or introduce noticeable lag
  • Test with tmux running inside the terminal session

Relevant code

  • crates/hero_router_ui/ — web admin dashboard (Axum + Askama)
  • PTY attach over WebSocket (existing feature per README)

Acceptance Criteria

  • Copy from remote terminal to local clipboard works
  • Paste from local clipboard into remote terminal works
  • Performance is acceptable (no heavy lag)
  • Works correctly with tmux running inside the session
## Problem Copy-paste between a remote user and the hero_router terminal (PTY over WebSocket) is not working smoothly. This was a blocker when using tmux remotely and is a hard requirement for replacing TeamWorks. ## Requirements - Copy-paste must work reliably between the user's local browser and the remote terminal session - Must not be heavy or introduce noticeable lag - Test with tmux running inside the terminal session ## Relevant code - `crates/hero_router_ui/` — web admin dashboard (Axum + Askama) - PTY attach over WebSocket (existing feature per README) ## Acceptance Criteria - [ ] Copy from remote terminal to local clipboard works - [ ] Paste from local clipboard into remote terminal works - [ ] Performance is acceptable (no heavy lag) - [ ] Works correctly with tmux running inside the session
Member

Implementation Spec for Issue #69 — Terminal: smooth copy/paste for remote users (PTY/WebSocket)

Objective

Make copy/paste between the user's local browser clipboard and the remote PTY session served by hero_router work reliably and with negligible lag, including when tmux runs inside the session. The fix stays inside the existing xterm.js + WebSocket pipeline, adds no new dependencies, and uses tmux's native OSC 52 channel so copy-pipe/copy-selection yanks land in the browser clipboard.

Requirements

  • Copy from the remote terminal lands in the local browser clipboard, in two ways:
    • Mouse selection inside the xterm.js canvas + Ctrl+Shift+C / right-click → Copy (already partially wired; keep working).
    • tmux yank (ESC ] 52 ; c ; <base64> BEL) — currently dropped, must be parsed and pushed to navigator.clipboard.writeText.
  • Paste from the local clipboard injects bytes into the PTY through the existing user_input JSON-RPC frame.
    • Paste must use bracketed paste mode so tmux/vim/bracketed-paste-aware shells distinguish paste from typing — that is what makes multi-line tmux pastes "smooth".
  • Plain Ctrl+C still sends SIGINT when there is no selection (do not regress).
  • No noticeable input-latency regression on term.onData.
  • Graceful fallback when navigator.clipboard.writeText/readText is unavailable (insecure context, Firefox readText off): hidden <textarea> + document.execCommand.
  • OSC 52 queries (ESC ] 52 ; c ; ? BEL) are explicitly ignored — never expose the user's clipboard back to the PTY.
  • Server side: ensure DSR-CPR stripping does not corrupt OSC 52 frames (it must not — ESC[6n and ESC]52 share only the ESC byte — but verify and pin with a unit test).

Files to Modify/Create

  • crates/hero_router/static/js/terminal.js — register term.parser.registerOscHandler(52, ...), switch paste path to term.paste(text) so bracketed-paste is honoured, add rightClickSelectsWord: true to the Terminal options, add copyToClipboard/readFromClipboard helpers with execCommand fallback, reuse them from both the keyboard handler and the right-click menu.
  • crates/hero_router/templates/terminal.html — add a "Shift+drag" row to the shortcuts modal so users know how to bypass tmux mouse mode for native selection.
  • crates/hero_router/src/server/terminal.rs — add a #[cfg(test)] mod tests with two regression tests pinning that strip_dsr_cpr_queries does not eat OSC 52 frames. No production-code change here.
  • crates/hero_router/README.md — append a "Clipboard & tmux" section documenting the recommended tmux config (set -g set-clipboard on, set -g mouse on), the Shift-drag gotcha, and the Ctrl+Shift+C/V keys.

Implementation Plan

Step 1 — Add clipboard helper + OSC 52 handler in terminal.js

Files: crates/hero_router/static/js/terminal.js
Subtasks:

  • Add two top-level helpers near bytesToB64 (around line 27):
    • copyToClipboard(text): try navigator.clipboard.writeText(text); on rejection or absence, fall back to a hidden <textarea> + document.execCommand('copy').
    • readFromClipboard(): try navigator.clipboard.readText(); on failure, return Promise.reject(...) so callers can show a non-fatal hint.
  • Inside createPane, after term.open(bodyEl) but before the attachCustomKeyEventHandler call, register an OSC 52 handler that decodes the base64 payload and calls copyToClipboard(decoded). Ignore queries (? payload), ignore non-c/s targets, and cap decoded size at ~1 MB.
    Dependencies: none.

Step 2 — Use term.paste(text) and tighten Terminal options

Files: crates/hero_router/static/js/terminal.js
Subtasks:

  • In the new Terminal({...}) block (around line 613), add rightClickSelectsWord: true and keep allowProposedApi: true (already present at line 619 — required by registerOscHandler).
  • Replace the two existing paste call sites — the right-click menu's "Paste" item (lines ~559-565) and the Ctrl+Shift+V branch in attachCustomKeyEventHandler (lines ~654-664) — so both call:
    readFromClipboard().then(function (text) {
      if (text && pane.term) pane.term.paste(text);
    }).catch(function () { /* user denied or unsupported */ });
    
  • term.paste(text) automatically wraps in ESC[200~ ... ESC[201~ when the underlying app has enabled bracketed paste, then emits the wrapped bytes through the same onData path the existing client→server scanner already consumes — no WS framing change required.
  • Make the right-click "Copy" item also go through copyToClipboard so behaviour is consistent across menus and key handler.
    Dependencies: Step 1 (uses the helpers).

Step 3 — Document the tmux gotcha in the shortcuts modal

Files: crates/hero_router/templates/terminal.html
Subtasks:

  • In the shortcuts modal (lines 287-311), add one row to the <table>:
    <tr><td><kbd>Shift</kbd>+drag</td><td>Force native selection (bypass tmux mouse mode)</td></tr>
  • Update the existing Ctrl+Shift+C / V row's description to: Copy selection / paste from clipboard (works under tmux via OSC 52).
    Dependencies: none.

Step 4 — Server-side regression test for OSC 52 vs DSR stripper

Files: crates/hero_router/src/server/terminal.rs
Subtasks:

  • Append #[cfg(test)] mod tests { ... } covering:
    • strip_dsr_cpr_queries_leaves_osc52_intact — input contains an OSC 52 frame, expect zero strips and bytes unchanged.
    • strip_dsr_cpr_queries_strips_only_dsr — input contains both ESC[6n and an OSC 52 frame, expect the DSR-CPR removed and the OSC 52 frame untouched.
  • No production-code change. Pins behaviour so future maintenance of the stripper cannot eat OSC 52.
    Dependencies: none.

Step 5 — User-facing tmux configuration docs

Files: crates/hero_router/README.md
Subtasks:

  • Append a "Clipboard & tmux" section covering:
    • For copy from the remote terminal to land in the browser clipboard, tmux must emit OSC 52: set -g set-clipboard on (or external on tmux ≥ 3.2). For older tmux, add set -ga terminal-overrides ',*:Ms=\\E]52;c;%p2%s\\7'.
    • For mouse selection inside tmux: set -g mouse on. To bypass tmux's mouse capture and use native xterm.js selection, hold Shift while click-dragging.
    • Keys: Ctrl+Shift+C to copy current selection, Ctrl+Shift+V to paste. Right-click menu offers the same.
    • Note that paste uses bracketed paste mode, so multi-line pastes don't auto-execute in shells with bracketed paste support.
      Dependencies: none.

Acceptance Criteria

  • Copy from remote terminal to local clipboard works (mouse selection + Ctrl+Shift+C, and tmux yank via OSC 52).
  • Paste from local clipboard into remote terminal works (Ctrl+Shift+V and right-click → Paste) and uses bracketed paste mode so tmux / vim / bracketed-paste-aware shells treat it as paste.
  • Performance is acceptable — paste of 10 KB feels instantaneous; no measurable input-latency regression.
  • Works correctly with tmux running inside the session, including in tmux copy-mode with set-clipboard on.
  • Plain Ctrl+C still sends SIGINT when no selection is active (no regression).
  • OSC 52 queries are not answered back (security).
  • cargo test -p hero_router passes including the two new DSR-vs-OSC52 tests.

Notes

  • tmux requirement: set -g set-clipboard on. Without it tmux never emits OSC 52, so the OSC 52 handler is a no-op and copy from inside tmux only works via xterm.js mouse selection + Ctrl+Shift+C. Documented in Step 5.
  • Browsers block navigator.clipboard.readText() outside a user gesture. Both call sites (keypress and contextmenu) are user gestures, so this is fine. Do not move paste into a timer / async callback chain.
  • xterm.js mouse reporting (used by set -g mouse on) consumes click events. Holding Shift bypasses it — documented in the shortcuts modal (Step 3) and README (Step 5).
  • Iframing: if the terminal page is embedded inside a Hero OS shell <iframe>, the embedding page must set allow="clipboard-read; clipboard-write". For the standalone admin dashboard at :9997 the page is top-level so no Permissions Policy work is needed.
  • OSC 52 payload cap: ~1 MB after base64 decode. Larger payloads are dropped silently rather than allocated, to neutralise a misbehaving PTY-side process.
  • The router's DSR-CPR stripper (strip_dsr_cpr_queries) only matches the exact 4-byte sequence ESC [ 6 n. OSC 52 starts with ESC ] (different intermediate byte), so the two cannot collide. Step 4 pins this with regression tests.
  • xterm.js shipped here already has allowProposedApi: true; term.parser.registerOscHandler is gated on it — keep the flag on.
  • The pending reply-buffer logic in term.onData only buffers prefixes that start with ESC [. OSC 52 (ESC ]) is unaffected, so client→server framing of paste data is correct.
## Implementation Spec for Issue #69 — Terminal: smooth copy/paste for remote users (PTY/WebSocket) ### Objective Make copy/paste between the user's local browser clipboard and the remote PTY session served by `hero_router` work reliably and with negligible lag, including when **tmux** runs inside the session. The fix stays inside the existing xterm.js + WebSocket pipeline, adds no new dependencies, and uses tmux's native OSC 52 channel so `copy-pipe`/`copy-selection` yanks land in the browser clipboard. ### Requirements - Copy from the remote terminal lands in the local browser clipboard, in two ways: - Mouse selection inside the xterm.js canvas + Ctrl+Shift+C / right-click → Copy (already partially wired; keep working). - tmux yank (`ESC ] 52 ; c ; <base64> BEL`) — currently dropped, must be parsed and pushed to `navigator.clipboard.writeText`. - Paste from the local clipboard injects bytes into the PTY through the existing `user_input` JSON-RPC frame. - Paste must use **bracketed paste mode** so tmux/vim/bracketed-paste-aware shells distinguish paste from typing — that is what makes multi-line tmux pastes "smooth". - Plain Ctrl+C still sends SIGINT when there is no selection (do not regress). - No noticeable input-latency regression on `term.onData`. - Graceful fallback when `navigator.clipboard.writeText/readText` is unavailable (insecure context, Firefox readText off): hidden `<textarea>` + `document.execCommand`. - OSC 52 *queries* (`ESC ] 52 ; c ; ? BEL`) are explicitly **ignored** — never expose the user's clipboard back to the PTY. - Server side: ensure DSR-CPR stripping does not corrupt OSC 52 frames (it must not — `ESC[6n` and `ESC]52` share only the ESC byte — but verify and pin with a unit test). ### Files to Modify/Create - `crates/hero_router/static/js/terminal.js` — register `term.parser.registerOscHandler(52, ...)`, switch paste path to `term.paste(text)` so bracketed-paste is honoured, add `rightClickSelectsWord: true` to the `Terminal` options, add `copyToClipboard`/`readFromClipboard` helpers with `execCommand` fallback, reuse them from both the keyboard handler and the right-click menu. - `crates/hero_router/templates/terminal.html` — add a "Shift+drag" row to the shortcuts modal so users know how to bypass tmux mouse mode for native selection. - `crates/hero_router/src/server/terminal.rs` — add a `#[cfg(test)] mod tests` with two regression tests pinning that `strip_dsr_cpr_queries` does not eat OSC 52 frames. **No production-code change here.** - `crates/hero_router/README.md` — append a "Clipboard & tmux" section documenting the recommended tmux config (`set -g set-clipboard on`, `set -g mouse on`), the Shift-drag gotcha, and the Ctrl+Shift+C/V keys. ### Implementation Plan #### Step 1 — Add clipboard helper + OSC 52 handler in `terminal.js` Files: `crates/hero_router/static/js/terminal.js` Subtasks: - Add two top-level helpers near `bytesToB64` (around line 27): - `copyToClipboard(text)`: try `navigator.clipboard.writeText(text)`; on rejection or absence, fall back to a hidden `<textarea>` + `document.execCommand('copy')`. - `readFromClipboard()`: try `navigator.clipboard.readText()`; on failure, return `Promise.reject(...)` so callers can show a non-fatal hint. - Inside `createPane`, after `term.open(bodyEl)` but before the `attachCustomKeyEventHandler` call, register an OSC 52 handler that decodes the base64 payload and calls `copyToClipboard(decoded)`. Ignore queries (`?` payload), ignore non-`c`/`s` targets, and cap decoded size at ~1 MB. Dependencies: none. #### Step 2 — Use `term.paste(text)` and tighten `Terminal` options Files: `crates/hero_router/static/js/terminal.js` Subtasks: - In the `new Terminal({...})` block (around line 613), add `rightClickSelectsWord: true` and keep `allowProposedApi: true` (already present at line 619 — required by `registerOscHandler`). - Replace the **two** existing paste call sites — the right-click menu's "Paste" item (lines ~559-565) and the Ctrl+Shift+V branch in `attachCustomKeyEventHandler` (lines ~654-664) — so both call: ``` readFromClipboard().then(function (text) { if (text && pane.term) pane.term.paste(text); }).catch(function () { /* user denied or unsupported */ }); ``` - `term.paste(text)` automatically wraps in `ESC[200~ ... ESC[201~` when the underlying app has enabled bracketed paste, then emits the wrapped bytes through the same `onData` path the existing client→server scanner already consumes — no WS framing change required. - Make the right-click "Copy" item also go through `copyToClipboard` so behaviour is consistent across menus and key handler. Dependencies: Step 1 (uses the helpers). #### Step 3 — Document the tmux gotcha in the shortcuts modal Files: `crates/hero_router/templates/terminal.html` Subtasks: - In the shortcuts modal (lines 287-311), add one row to the `<table>`: `<tr><td><kbd>Shift</kbd>+drag</td><td>Force native selection (bypass tmux mouse mode)</td></tr>` - Update the existing `Ctrl+Shift+C / V` row's description to: `Copy selection / paste from clipboard (works under tmux via OSC 52)`. Dependencies: none. #### Step 4 — Server-side regression test for OSC 52 vs DSR stripper Files: `crates/hero_router/src/server/terminal.rs` Subtasks: - Append `#[cfg(test)] mod tests { ... }` covering: - `strip_dsr_cpr_queries_leaves_osc52_intact` — input contains an OSC 52 frame, expect zero strips and bytes unchanged. - `strip_dsr_cpr_queries_strips_only_dsr` — input contains both `ESC[6n` and an OSC 52 frame, expect the DSR-CPR removed and the OSC 52 frame untouched. - No production-code change. Pins behaviour so future maintenance of the stripper cannot eat OSC 52. Dependencies: none. #### Step 5 — User-facing tmux configuration docs Files: `crates/hero_router/README.md` Subtasks: - Append a "Clipboard & tmux" section covering: - For copy from the remote terminal to land in the browser clipboard, tmux must emit OSC 52: `set -g set-clipboard on` (or `external` on tmux ≥ 3.2). For older tmux, add `set -ga terminal-overrides ',*:Ms=\\E]52;c;%p2%s\\7'`. - For mouse selection inside tmux: `set -g mouse on`. To bypass tmux's mouse capture and use native xterm.js selection, hold **Shift** while click-dragging. - Keys: `Ctrl+Shift+C` to copy current selection, `Ctrl+Shift+V` to paste. Right-click menu offers the same. - Note that paste uses bracketed paste mode, so multi-line pastes don't auto-execute in shells with bracketed paste support. Dependencies: none. ### Acceptance Criteria - [ ] Copy from remote terminal to local clipboard works (mouse selection + Ctrl+Shift+C, **and** tmux yank via OSC 52). - [ ] Paste from local clipboard into remote terminal works (Ctrl+Shift+V and right-click → Paste) and uses bracketed paste mode so tmux / vim / bracketed-paste-aware shells treat it as paste. - [ ] Performance is acceptable — paste of 10 KB feels instantaneous; no measurable input-latency regression. - [ ] Works correctly with tmux running inside the session, including in tmux copy-mode with `set-clipboard on`. - [ ] Plain Ctrl+C still sends SIGINT when no selection is active (no regression). - [ ] OSC 52 *queries* are not answered back (security). - [ ] `cargo test -p hero_router` passes including the two new DSR-vs-OSC52 tests. ### Notes - tmux requirement: `set -g set-clipboard on`. Without it tmux never emits OSC 52, so the OSC 52 handler is a no-op and copy from inside tmux only works via xterm.js mouse selection + Ctrl+Shift+C. Documented in Step 5. - Browsers block `navigator.clipboard.readText()` outside a user gesture. Both call sites (keypress and contextmenu) are user gestures, so this is fine. Do not move paste into a timer / async callback chain. - xterm.js mouse reporting (used by `set -g mouse on`) consumes click events. Holding Shift bypasses it — documented in the shortcuts modal (Step 3) and README (Step 5). - Iframing: if the terminal page is embedded inside a Hero OS shell `<iframe>`, the embedding page must set `allow="clipboard-read; clipboard-write"`. For the standalone admin dashboard at `:9997` the page is top-level so no Permissions Policy work is needed. - OSC 52 payload cap: ~1 MB after base64 decode. Larger payloads are dropped silently rather than allocated, to neutralise a misbehaving PTY-side process. - The router's DSR-CPR stripper (`strip_dsr_cpr_queries`) only matches the exact 4-byte sequence `ESC [ 6 n`. OSC 52 starts with `ESC ]` (different intermediate byte), so the two cannot collide. Step 4 pins this with regression tests. - xterm.js shipped here already has `allowProposedApi: true`; `term.parser.registerOscHandler` is gated on it — keep the flag on. - The `pending` reply-buffer logic in `term.onData` only buffers prefixes that start with `ESC [`. OSC 52 (`ESC ]`) is unaffected, so client→server framing of paste data is correct.
Member

Test Results

cargo test -p hero_router

  • Total: 81
  • Passed: 81
  • Failed: 0
  • Ignored: 0

Including two new regression tests added by this change in crates/hero_router/src/server/terminal.rs:

  • server::terminal::tests::strip_dsr_cpr_queries_leaves_osc52_intact
  • server::terminal::tests::strip_dsr_cpr_queries_strips_only_dsr

These pin that the DSR cursor-position stripper does not consume OSC 52 frames (ESC ] 52 ; c ; <base64> BEL) emitted by tmux yanks — that is the channel the new browser-side OSC 52 handler relies on.

## Test Results `cargo test -p hero_router` - Total: 81 - Passed: 81 - Failed: 0 - Ignored: 0 Including two new regression tests added by this change in `crates/hero_router/src/server/terminal.rs`: - `server::terminal::tests::strip_dsr_cpr_queries_leaves_osc52_intact` - `server::terminal::tests::strip_dsr_cpr_queries_strips_only_dsr` These pin that the DSR cursor-position stripper does not consume OSC 52 frames (`ESC ] 52 ; c ; <base64> BEL`) emitted by tmux yanks — that is the channel the new browser-side OSC 52 handler relies on.
Member

Implementation Summary

Branch: development_terminal_clipboard_tmux

Changes

crates/hero_router/static/js/terminal.js — main fix

  • New top-level helpers: copyToClipboard(text), readFromClipboard(), with a hidden-<textarea> + document.execCommand fallback when the async Clipboard API is unavailable (insecure context, Firefox readText off).
  • New per-pane OSC 52 handler via term.parser.registerOscHandler(52, ...) — decodes tmux yanks (ESC ] 5 2 ; c ; <base64> BEL) and routes them to the local clipboard. Targets restricted to c and s; queries (? payload) deliberately ignored to prevent the remote PTY from reading the user's clipboard. Decoded payloads capped at ~1 MB.
  • Paste is now routed through term.paste(text) so xterm.js wraps it in ESC[200~ ... ESC[201~ when bracketed-paste mode is on. tmux, vim, and bracketed-paste-aware shells now treat multi-line pastes as paste, not typing — that is the lag/edge-case the issue called out.
  • Both call sites (Ctrl+Shift+V and right-click → Paste) and both copy sites (Ctrl+Shift+C and right-click → Copy) now go through the shared helpers.
  • Added rightClickSelectsWord: true to the Terminal constructor.

crates/hero_router/templates/terminal.html

  • Shortcuts modal: updated the Ctrl+Shift+C / V row description, added a Shift+drag row documenting how to bypass tmux mouse mode for native xterm.js selection.

crates/hero_router/src/server/terminal.rs

  • Added #[cfg(test)] mod tests with two regression tests pinning that strip_dsr_cpr_queries does not consume OSC 52 frames. No production-code change.

README.md

  • New "Clipboard & tmux (admin terminal)" section: required tmux config (set -g mouse on, set -g set-clipboard on, terminal-overrides line for older tmux), key bindings, and the Shift-drag tip.

Test Results

cargo test -p hero_router — 81 passed, 0 failed, 0 ignored. Includes both new regression tests.

Manual Verification Checklist

  • In a tmux session: select with mouse, hit prefix + ] after copy-mode yank — text appears in local clipboard.
  • Local clipboard → terminal: Ctrl+Shift+V and right-click → Paste; verify multi-line paste does not auto-execute under bash 4.4+ / zsh / vim insert-mode.
  • Plain Ctrl+C still sends SIGINT when no selection is active.
  • Shift+drag selects text natively even with set -g mouse on.

Notes

  • No new dependencies.
  • Server-side WS framing unchanged — paste rides the existing user_input JSON frame.
  • Iframe embedding (Hero OS shell) needs allow="clipboard-read; clipboard-write" on the embedding <iframe>. The standalone admin dashboard at :9997 is top-level and does not need any Permissions Policy work.
## Implementation Summary Branch: `development_terminal_clipboard_tmux` ### Changes **`crates/hero_router/static/js/terminal.js`** — main fix - New top-level helpers: `copyToClipboard(text)`, `readFromClipboard()`, with a hidden-`<textarea>` + `document.execCommand` fallback when the async Clipboard API is unavailable (insecure context, Firefox readText off). - New per-pane OSC 52 handler via `term.parser.registerOscHandler(52, ...)` — decodes tmux yanks (`ESC ] 5 2 ; c ; <base64> BEL`) and routes them to the local clipboard. Targets restricted to `c` and `s`; queries (`?` payload) deliberately ignored to prevent the remote PTY from reading the user's clipboard. Decoded payloads capped at ~1 MB. - Paste is now routed through `term.paste(text)` so xterm.js wraps it in `ESC[200~ ... ESC[201~` when bracketed-paste mode is on. tmux, vim, and bracketed-paste-aware shells now treat multi-line pastes as paste, not typing — that is the lag/edge-case the issue called out. - Both call sites (Ctrl+Shift+V and right-click → Paste) and both copy sites (Ctrl+Shift+C and right-click → Copy) now go through the shared helpers. - Added `rightClickSelectsWord: true` to the `Terminal` constructor. **`crates/hero_router/templates/terminal.html`** - Shortcuts modal: updated the `Ctrl+Shift+C / V` row description, added a `Shift+drag` row documenting how to bypass tmux mouse mode for native xterm.js selection. **`crates/hero_router/src/server/terminal.rs`** - Added `#[cfg(test)] mod tests` with two regression tests pinning that `strip_dsr_cpr_queries` does not consume OSC 52 frames. No production-code change. **`README.md`** - New "Clipboard & tmux (admin terminal)" section: required tmux config (`set -g mouse on`, `set -g set-clipboard on`, terminal-overrides line for older tmux), key bindings, and the Shift-drag tip. ### Test Results `cargo test -p hero_router` — 81 passed, 0 failed, 0 ignored. Includes both new regression tests. ### Manual Verification Checklist - [ ] In a `tmux` session: select with mouse, hit `prefix + ]` after copy-mode yank — text appears in local clipboard. - [ ] Local clipboard → terminal: `Ctrl+Shift+V` and right-click → Paste; verify multi-line paste does not auto-execute under bash 4.4+ / zsh / vim insert-mode. - [ ] Plain `Ctrl+C` still sends SIGINT when no selection is active. - [ ] `Shift`+drag selects text natively even with `set -g mouse on`. ### Notes - No new dependencies. - Server-side WS framing unchanged — paste rides the existing `user_input` JSON frame. - Iframe embedding (Hero OS shell) needs `allow="clipboard-read; clipboard-write"` on the embedding `<iframe>`. The standalone admin dashboard at `:9997` is top-level and does not need any Permissions Policy work.
Member

Pull request opened: #75

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_router/pulls/75 This PR implements the changes discussed in this issue.
Member

Implementation Spec for Issue #69 (continuation) — yanking still not reaching local clipboard

Root cause

The PR shipped on this branch already wires up:

  • Browser-side term.parser.registerOscHandler(52, ...) decoding tmux yanks.
  • Bracketed-paste via term.paste(text).
  • set-clipboard on on the launched tmux.
  • mode-keys vi, with y / Enter / MouseDragEnd1Pane rebound to copy-pipe-no-clear so OSC 52 is the only sink.
  • getActiveSelection(term) falling back to window.getSelection() for Shift+drag under tmux mouse mode.
  • A regression test pinning that the DSR-CPR stripper does not consume OSC 52 frames.

The remaining failure is one layer below: from man tmux,

set-clipboard ... attempt to set the terminal clipboard ... **if there is an Ms entry in the terminfo description**.

tmux 3.4 — even with set-clipboard on or external — only emits OSC 52 when terminfo advertises the Ms extended capability. The TERM values commonly used with xterm.js (xterm-256color, xterm, screen-256color) do not carry Ms in the host terminfo. So tmux silently drops every yank — no OSC 52 ever leaves tmux, the browser handler never fires, and the user perceives "I can select but yanking is not working".

The user-pasted console errors are unrelated:

  • content.js:1 ... FILE_ERROR_NO_SPACE (... WritableFileAppend) is from a Chrome extension's IndexedDB (content scripts named content.js), not from the page.
  • unpoly.min.js ... Cannot read properties of undefined (reading 'closest') fires inside a setInterval-driven up.reload on terminal:704. It is a separate live-reload edge case on the terminal page; out of scope for clipboard.

Files to Modify

  • crates/hero_router/src/server/terminal.rs — extend the bootstrap tmux argv to inject Ms via terminal-overrides before new-session. Extract the argv into a constant + helper, add a regression test pinning the line.
  • crates/hero_router/README.md — append one paragraph to the existing "Clipboard & tmux" section explaining the Ms/terminal-overrides requirement.

Implementation Plan

Step 1 — Inject Ms via terminal-overrides in the bootstrap tmux argv

Files: crates/hero_router/src/server/terminal.rs
Subtasks:

  • In the ShellType::Tmux ActionBuilder::new argv (lines ~274-287), insert one clause before new-session:
    set-option -ga terminal-overrides ,*:Ms=\E]52;c;%p2%s\7 ;
    
    -ga = "global, append" so any existing per-host override is preserved.
  • The escapes \E and \7 are tmux's own format-string escapes (parsed by tmux, not the shell) so they survive interpreter("exec") whitespace argv-splitting as literal characters.
  • Keep all existing options (set-clipboard on, mouse on, mode-keys vi, the three rebound copy-mode keys).
  • Update the doc comment above the block to call out the Ms injection and link to issue #69.
  • Extract the full bootstrap string into a const TMUX_BOOTSTRAP_ARGV: &str = "..." near the top of the module so the test in Step 2 can reference the same source-of-truth.
    Dependencies: none.

Step 2 — Regression test pinning the bootstrap argv

Files: crates/hero_router/src/server/terminal.rs
Subtasks:

  • Add a test in the existing #[cfg(test)] mod tests that asserts TMUX_BOOTSTRAP_ARGV contains exactly:
    • set-clipboard on
    • terminal-overrides ,*:Ms=\\E]52;c;%p2%s\\7 (with \\ as Rust string escape — literal backslash + capital E)
    • mode-keys vi
    • new-session (and that it appears last)
  • Pins the bootstrap so a future cleanup can't quietly drop the Ms injection.
    Dependencies: Step 1.

Step 3 — README note

Files: crates/hero_router/README.md
Subtasks:

  • Append one short paragraph to the existing "Clipboard & tmux" section explaining: tmux's set-clipboard only emits OSC 52 when terminfo advertises Ms. hero_router-launched tmux sessions inject Ms via terminal-overrides, so OSC 52 yanks work regardless of the host's terminfo. If you tmux attach from outside hero_router, ensure your own tmux config does the same.
    Dependencies: none.

Acceptance Criteria

  • Inside a hero_router tmux session, mouse-drag-select then release yanks the selection into the local browser clipboard.
  • In tmux copy-mode (prefix + [, then v / motion / y), the yank lands in the local browser clipboard.
  • cargo test -p hero_router passes, including the new tmux_bootstrap_contains_ms_terminal_override regression test.
  • Existing OSC 52 / DSR-CPR tests still pass.

Notes

  • This is the only missing piece in an otherwise-complete pipeline — every other layer is already correct on development_terminal_clipboard_tmux. Injecting Ms via terminal-overrides is what unblocks tmux's OSC 52 emission.
  • Why not switch set-clipboard on to external? Same Ms requirement applies — external only changes whether tmux also accepts app-side OSC 52 to populate its own buffers, not whether tmux emits. Both modes need Ms to emit.
  • Console errors the user pasted (FILE_ERROR_NO_SPACE, unpoly closest) are unrelated to clipboard and out of scope. Happy to open a separate issue for the unpoly periodic-reload error if it's worth chasing.
## Implementation Spec for Issue #69 (continuation) — yanking still not reaching local clipboard ### Root cause The PR shipped on this branch already wires up: - Browser-side `term.parser.registerOscHandler(52, ...)` decoding tmux yanks. - Bracketed-paste via `term.paste(text)`. - `set-clipboard on` on the launched tmux. - `mode-keys vi`, with `y` / `Enter` / `MouseDragEnd1Pane` rebound to `copy-pipe-no-clear` so OSC 52 is the only sink. - `getActiveSelection(term)` falling back to `window.getSelection()` for Shift+drag under tmux mouse mode. - A regression test pinning that the DSR-CPR stripper does not consume OSC 52 frames. The remaining failure is one layer below: from `man tmux`, > `set-clipboard ... attempt to set the terminal clipboard ... **if there is an Ms entry in the terminfo description**.` tmux 3.4 — even with `set-clipboard on` or `external` — only emits OSC 52 when terminfo advertises the `Ms` extended capability. The TERM values commonly used with xterm.js (`xterm-256color`, `xterm`, `screen-256color`) do **not** carry `Ms` in the host terminfo. So tmux silently drops every yank — no OSC 52 ever leaves tmux, the browser handler never fires, and the user perceives "I can select but yanking is not working". The user-pasted console errors are unrelated: - `content.js:1 ... FILE_ERROR_NO_SPACE (... WritableFileAppend)` is from a Chrome extension's IndexedDB (content scripts named `content.js`), not from the page. - `unpoly.min.js ... Cannot read properties of undefined (reading 'closest')` fires inside a `setInterval`-driven `up.reload` on `terminal:704`. It is a separate live-reload edge case on the terminal page; out of scope for clipboard. ### Files to Modify - `crates/hero_router/src/server/terminal.rs` — extend the bootstrap tmux argv to inject `Ms` via `terminal-overrides` **before** `new-session`. Extract the argv into a constant + helper, add a regression test pinning the line. - `crates/hero_router/README.md` — append one paragraph to the existing "Clipboard & tmux" section explaining the `Ms`/terminal-overrides requirement. ### Implementation Plan #### Step 1 — Inject `Ms` via `terminal-overrides` in the bootstrap tmux argv Files: `crates/hero_router/src/server/terminal.rs` Subtasks: - In the `ShellType::Tmux` `ActionBuilder::new` argv (lines ~274-287), insert one clause **before** `new-session`: ``` set-option -ga terminal-overrides ,*:Ms=\E]52;c;%p2%s\7 ; ``` `-ga` = "global, append" so any existing per-host override is preserved. - The escapes `\E` and `\7` are tmux's own format-string escapes (parsed by tmux, not the shell) so they survive `interpreter("exec")` whitespace argv-splitting as literal characters. - Keep all existing options (`set-clipboard on`, `mouse on`, `mode-keys vi`, the three rebound copy-mode keys). - Update the doc comment above the block to call out the `Ms` injection and link to issue #69. - Extract the full bootstrap string into a `const TMUX_BOOTSTRAP_ARGV: &str = "..."` near the top of the module so the test in Step 2 can reference the same source-of-truth. Dependencies: none. #### Step 2 — Regression test pinning the bootstrap argv Files: `crates/hero_router/src/server/terminal.rs` Subtasks: - Add a test in the existing `#[cfg(test)] mod tests` that asserts `TMUX_BOOTSTRAP_ARGV` contains exactly: - `set-clipboard on` - `terminal-overrides ,*:Ms=\\E]52;c;%p2%s\\7` (with `\\` as Rust string escape — literal backslash + capital E) - `mode-keys vi` - `new-session` (and that it appears last) - Pins the bootstrap so a future cleanup can't quietly drop the `Ms` injection. Dependencies: Step 1. #### Step 3 — README note Files: `crates/hero_router/README.md` Subtasks: - Append one short paragraph to the existing "Clipboard & tmux" section explaining: tmux's `set-clipboard` only emits OSC 52 when terminfo advertises `Ms`. hero_router-launched tmux sessions inject `Ms` via `terminal-overrides`, so OSC 52 yanks work regardless of the host's terminfo. If you `tmux attach` from outside hero_router, ensure your own tmux config does the same. Dependencies: none. ### Acceptance Criteria - [ ] Inside a hero_router tmux session, mouse-drag-select then release yanks the selection into the local browser clipboard. - [ ] In tmux copy-mode (`prefix + [`, then `v` / motion / `y`), the yank lands in the local browser clipboard. - [ ] `cargo test -p hero_router` passes, including the new `tmux_bootstrap_contains_ms_terminal_override` regression test. - [ ] Existing OSC 52 / DSR-CPR tests still pass. ### Notes - This is the only missing piece in an otherwise-complete pipeline — every other layer is already correct on `development_terminal_clipboard_tmux`. Injecting `Ms` via `terminal-overrides` is what unblocks tmux's OSC 52 emission. - Why not switch `set-clipboard on` to `external`? Same `Ms` requirement applies — `external` only changes whether tmux also accepts app-side `OSC 52` to populate its own buffers, not whether tmux *emits*. Both modes need `Ms` to emit. - Console errors the user pasted (`FILE_ERROR_NO_SPACE`, unpoly `closest`) are unrelated to clipboard and out of scope. Happy to open a separate issue for the unpoly periodic-reload error if it's worth chasing.
ashraf self-assigned this 2026-04-30 08:46:26 +00:00
mahmoud added this to the ACTIVE project 2026-05-10 06:34:19 +00:00
mahmoud added this to the now milestone 2026-05-10 06:34:22 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router#69
No description provided.