Skip to content

remote-tmux: strip the screen/tmux ESC k window-title escape from mirror output - #7023

Merged
austinywang merged 3 commits into
manaflow-ai:mainfrom
ejc3:ejc3/remote-tmux-screen-title
Jul 1, 2026
Merged

austinywang merged 3 commits into
manaflow-ai:mainfrom
ejc3:ejc3/remote-tmux-screen-title

Conversation

@ejc3

@ejc3 ejc3 commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A remote shell running inside tmux sees TERM=screen*/tmux*, so its prompt (e.g. oh-my-zsh) sets the window title with the GNU screen escape ESC k <title> ESC \ rather than the xterm OSC. %output is the raw pty copy, so tmux forwards those bytes verbatim and only interprets them for its own window_name — its rendered pane (capture-pane) has the title stripped. cmux's mirror surface is an xterm-style emulator that doesn't recognize ESC k, so it printed the title text onto the screen:

typed:   echo "ej"
remote:  ej
cmux:    echoej     ← the prompt/preexec title "echo" leaked onto the output line

Pre-existing in both the per-session and linked-view remote-tmux mirrors. Plain shells (no title-setting) were unaffected.

Fix

RemoteTmuxScreenTitleFilter — a per-pane, stateful byte filter (survives %output chunk splits at any byte) applied in RemoteTmuxSessionMirror.routeOutput, dropping ESC k … ESC \. It terminates only on ST (ESC \), never BEL, matching tmux/screen exactly (an unterminated title consumes until ST, just like tmux's own screen).

  • Hot-path fast-path: chunks with no ESC (the overwhelmingly common case) pass through with no copy/allocation.
  • Reconnect-safe: filter state resets on the connection's non-connected transitions, so a reconnect's reseedAfterReconnect (synthetic clear/capture bytes) can't be swallowed by a filter left mid-title before the drop.

Verification

Exhaustive battery diffing cmux's render (read-screen) against tmux's ground truth (capture-pane) across 32 escape payloads — titles, OSC, DCS/APC/PM/SOS, SGR 16/256/truecolor, cursor/scroll/edit ops, erase, alt-screen, DEC charset, unicode/wide/emoji/zero-width — all render byte-identical (the only diffs are capture-pane vs read-screen serialization of tabs / DEC line-draw / zero-width, which are visually identical). Plus unit tests for the filter (chunk-split fuzzing, ST-only termination, passthrough of CSI/OSC/other escapes).


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Strip the GNU screen/tmux window-title escape ESC k <title> ESC \ from remote-tmux mirror output so title text no longer prints (e.g., echoej). Matches tmux pane rendering and survives chunk splits.

  • Bug Fixes

    • Added RemoteTmuxScreenTitleFilter, applied per-pane in RemoteTmuxSessionMirror.routeOutput; stateful across %output splits, terminates only on ST (never BEL), fast-paths chunks without ESC, builds into a [UInt8] buffer, resets on disconnect, and prunes on pane removal. Hooked onConnectionStateChanged to clear filter state when not connected.
    • Added tests that assert on raw Data, covering split boundaries, BEL-not-terminating behavior, back-to-back titles, and passthrough of other escapes.
  • Refactors

    • Lifted the filter into the CmuxRemoteSession package (pure logic) and moved mirror helpers into RemoteTmuxSessionMirror+Helpers.swift. Updated the project to satisfy file-budget and package policy.

Written for commit 537ebc8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a stateful streaming title filter that strips tmux/screen window-title escape sequences (ESC k <title> ESC \) from mirrored terminal output, preserving correct behavior across chunk boundaries and per-pane streams.
  • Bug Fixes
    • Prevents stale mid-sequence state by clearing the filter cache when the connection isn’t active.
    • Prunes per-pane cached state during rebuilds and ensures cleaned output is routed to the correct window/panel.
  • Tests
    • Added byte-for-byte test coverage for title stripping, BEL handling, back-to-back sequences, and extreme chunking.

@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

@ejc3 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds RemoteTmuxScreenTitleFilter for stripping ESC k <title> ESC \ sequences across data chunks, and wires it into RemoteTmuxSessionMirror with per-pane cached state, disconnect clearing, rebuild pruning, and helper extraction.

Changes

Remote Tmux Screen Title Filter

Layer / File(s) Summary
RemoteTmuxScreenTitleFilter state machine
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemoteTmuxScreenTitleFilter.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteTmuxScreenTitleFilterTests.swift
Defines RemoteTmuxScreenTitleFilter with internal parsing state and filter(_:) byte handling for ESC k title stripping across chunks; tests cover ST termination, BEL handling, split boundaries, and passthrough cases.
Mirror helper extraction and project wiring
Sources/RemoteTmuxSessionMirror+Helpers.swift, Sources/RemoteTmuxSessionMirror.swift, cmux.xcodeproj/project.pbxproj
Moves mirror helper logic into an extension file, removes the class-local reorder helper, and adds the new source file to the Xcode project.
Per-pane filter wiring in RemoteTmuxSessionMirror
Sources/RemoteTmuxSessionMirror.swift
Adds a titleFilters cache, clears it when disconnected, prunes stale pane entries in rebuild(), and routes filtered output instead of raw bytes.

Sequence Diagram(s)

sequenceDiagram
  participant tmux as Remote tmux
  participant Mirror as RemoteTmuxSessionMirror
  participant Filter as RemoteTmuxScreenTitleFilter
  participant Surface as WindowMirror or TerminalPanel

  tmux->>Mirror: routeOutput(paneId, raw data)
  Mirror->>Filter: filter(raw data)
  Filter-->>Mirror: cleaned data
  Mirror->>Surface: route cleaned data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hop, hop—through bytes we weave,
ESC k titles vanish like autumn leaves.
The mirror stays tidy, the panes stay bright,
Fresh output glides in, all crisp and right.

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: stripping tmux/screen ESC k title escapes from mirror output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: new mutable state stays inside @MainActor mirror/connection types, helpers are explicitly nonisolated, and no new shared Sendable or background UI-store access appears.
Cmux Swift Blocking Runtime ✅ Passed No new waits, sleeps, locks, or sync calls were added; the commit only extracts pure helpers and updates the Xcode project.
Cmux Browser Automation Off-Main ✅ Passed PR only changes remote-tmux mirroring/filter/tests and the Xcode project; it does not touch the browser socket automation paths named in the rule.
Cmux Expensive Synchronous Load ✅ Passed The diff only adds tmux output filtering/helpers and tests; no agent-history loads, JSON parsing, directory scans, or other expensive sync work were introduced on MainActor.
Cmux Cache Substitution Correctness ✅ Passed titleFilters is transient live-output parsing state, reset on disconnect and pruned on topology changes; it doesn’t replace any persisted/history/snapshot read.
Cmux No Hacky Sleeps ✅ Passed PR changes only Swift sources and an Xcode project file; the no-hacky-sleeps rule is scoped to TS/JS/shell and non-Swift runtime scripts, so it doesn’t apply.
Cmux Algorithmic Complexity ✅ Passed Added byte-stream filter and linear cache pruning only; no nested rescans or repeated full-collection sorting/filtering in hot paths.
Cmux Swift Concurrency ✅ Passed No new legacy async patterns were introduced: the new filter/helpers are synchronous, and existing Task uses are boundary hops or stored/cancelled.
Cmux Swift @Concurrent ✅ Passed No changed async helper or @concurrent usage appears in the patch; new helpers are synchronous/pure and the routing code stays on @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed PASS: the stateful title-stripper lives in the CmuxRemoteSession package; the app-target mirror only adds small routing/cache glue and did not materially grow.
Cmux Swiftpm Lockfiles ✅ Passed Only source files and a source-list tweak in cmux.xcodeproj changed; no Package.swift, Package.resolved, .gitignore, or workflow dependency edits.
Cmux Swift Logging ✅ Passed The modified Swift files add no print/debugPrint/dump/NSLog or Logger usage; the only 'print' hits are in comments.
Cmux User-Facing Error Privacy ✅ Passed Touched files add a tmux title filter and tests/comments only; no user-facing errors, alerts, API bodies, or recovery copy changed.
Cmux Full Internationalization ✅ Passed PASS: The diff only extracts remote-tmux helpers and updates pbxproj; no new user-facing strings, xcstrings, Info.plist, or web locale assets were added.
Cmux Swiftui State Layout ✅ Passed PASS: The diff only changes tmux mirror/filter logic and tests; touched files import Foundation/CmuxRemoteSession and contain no SwiftUI views, ObservableObject, GeometryReader, or layout state.
Cmux Architecture Rethink ✅ Passed The PR keeps ownership local in RemoteTmuxSessionMirror, adds a clear per-pane filter invariant, and uses a documented connection-state callback to reset cache state.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched files are remote-tmux routing/filter code; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes were added, so the shortcut rule doesn’t apply.
Cmux Source Artifacts ✅ Passed All touched paths are deliberate Swift source/test/config files; none are logs, caches, screenshots, temp folders, or other checked-in artifacts per the rule.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production files add real runtime helpers/filtering, with no #if DEBUG, debug/test-hook names, or test-only accessors in Sources.
Cmux No Ambient Global State ✅ Passed PASS: No new top-level mutable state or singleton was added; the new cache is instance-owned, and the helpers are methods on an existing type, not an ambient namespace.
Description check ✅ Passed The description clearly covers the problem, fix, and verification, with only template sections like testing, demo, and checklist missing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ejc3
ejc3 marked this pull request as ready for review June 28, 2026 05:20
@greptile-apps

greptile-apps Bot commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a terminal rendering bug where shell prompts running inside tmux (with TERM=screen*/tmux*) emit the GNU screen ESC k <title> ESC \ window-title escape that cmux's xterm-style mirror surface didn't recognize, causing title text to appear inline in the rendered output (e.g. echoej). The fix adds a RemoteTmuxScreenTitleFilter to strip those sequences before mirrored output is rendered.

  • RemoteTmuxScreenTitleFilter: a new per-pane, stateful byte-stream filter in the CmuxRemoteSession package, with a hot-path fast-pass for ESC-free chunks and correct handling of %output chunk splits at any byte boundary.
  • RemoteTmuxSessionMirror: applies the filter per-pane in routeOutput (covering both single-pane and multi-pane paths), prunes filter state on topology rebuild, and resets all filter state on any non-connected transition via the existing onConnectionStateChanged hook.
  • Two static helpers (shouldSeedSinglePaneDisplay, mirrorTabReorder) moved to RemoteTmuxSessionMirror+Helpers.swift as a separate file in the app target.

Confidence Score: 5/5

Safe to merge — the filter is correctly applied to all output paths, state resets properly on disconnect, and the logic is fully covered by chunk-split fuzzing and edge-case tests.

The change is narrow and well-contained: a new value-type byte filter applied in one call site, with no actor isolation issues (all mutations stay on the main actor), correct pruning alongside the existing cwdByPane cache, and an existing connection-state hook used to clear stale filter state before reseed. The state machine correctly handles consecutive ESCs, BEL-not-terminating, and every chunk-split boundary. No blocking primitives, no ambient globals, and no test seams in production source.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemoteTmuxScreenTitleFilter.swift New stateful byte filter; hot-path fast-path is correct, state machine handles all chunk-split and consecutive-ESC edge cases correctly, buffer sizing via reserveCapacity is appropriate.
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemoteTmuxScreenTitleFilterState.swift Four-case enum for the filter parser state; correctly scoped as internal since state is private inside the public struct.
Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteTmuxScreenTitleFilterTests.swift Thorough tests covering the echoej repro, BEL-not-terminating, multi-title stripping, every-byte chunk-split fuzzing, CSI/OSC passthrough, and back-to-back titles.
Sources/RemoteTmuxSessionMirror.swift Filter applied in routeOutput before both single-pane and multi-pane dispatch paths; per-pane state correctly stored back after each mutation; pruned in rebuild() alongside cwdByPane; reset on disconnect via onConnectionStateChanged.
Sources/RemoteTmuxSessionMirror+Helpers.swift Mechanical move of shouldSeedSinglePaneDisplay and mirrorTabReorder static helpers into a dedicated file; logic is unchanged.
cmux.xcodeproj/project.pbxproj Adds RemoteTmuxSessionMirror+Helpers.swift to both file references and the build sources phase; functionally correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant tmux as Remote tmux (%output)
    participant conn as RemoteTmuxControlConnection
    participant mirror as RemoteTmuxSessionMirror
    participant filter as RemoteTmuxScreenTitleFilter (per pane)
    participant surface as TerminalSurface / WindowMirror

    tmux->>conn: raw pty bytes (may contain ESC k title ESC \\)
    conn->>mirror: onPaneOutput(paneId, data)
    mirror->>filter: filter(data)
    note over filter: fast-path: no ESC then return data<br/>otherwise: strip ESC k ESC\\ sequences<br/>stateful across chunk splits
    filter-->>mirror: cleaned Data
    mirror->>surface: routeOutput / processRemoteOutput(cleaned)

    note over mirror: onConnectionStateChanged not connected<br/>titleFilters.removeAll()
    note over mirror: rebuild() titleFilters.filter(livePanes)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant tmux as Remote tmux (%output)
    participant conn as RemoteTmuxControlConnection
    participant mirror as RemoteTmuxSessionMirror
    participant filter as RemoteTmuxScreenTitleFilter (per pane)
    participant surface as TerminalSurface / WindowMirror

    tmux->>conn: raw pty bytes (may contain ESC k title ESC \\)
    conn->>mirror: onPaneOutput(paneId, data)
    mirror->>filter: filter(data)
    note over filter: fast-path: no ESC then return data<br/>otherwise: strip ESC k ESC\\ sequences<br/>stateful across chunk splits
    filter-->>mirror: cleaned Data
    mirror->>surface: routeOutput / processRemoteOutput(cleaned)

    note over mirror: onConnectionStateChanged not connected<br/>titleFilters.removeAll()
    note over mirror: rebuild() titleFilters.filter(livePanes)
Loading

Reviews (7): Last reviewed commit: "Satisfy remote tmux filter package polic..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmux.xcodeproj/project.pbxproj`:
- Line 3096: RemoteTmuxScreenTitleFilter is standalone parsing/state-machine
logic that should not live in the app target root Sources. Move the
RemoteTmuxScreenTitleFilter.swift implementation into an existing remote/session
package target or a small new reusable module, then update the app target to
depend on that module instead of owning the type directly. Keep the type name
and any callers/imports consistent so the app still references
RemoteTmuxScreenTitleFilter from the new package boundary.

In `@cmuxTests/RemoteTmuxScreenTitleFilterTests.swift`:
- Around line 16-22: The test helper in RemoteTmuxScreenTitleFilterTests is
decoding filtered output with String(decoding:as:), which can hide invalid UTF-8
and mask byte-stream regressions. Update the run(_:) helpers to assert on the
raw Data returned from RemoteTmuxScreenTitleFilter.filter, or switch to a
throwing/optional UTF-8 conversion such as String(bytes:encoding:) so malformed
output fails instead of being silently replaced.

In `@Sources/RemoteTmuxScreenTitleFilter.swift`:
- Around line 36-77: The hot path in `RemoteTmuxScreenTitleFilter.filter(_:)` is
doing per-byte `Data.append`, which is too costly for `%output` chunks that
contain frequent ANSI escape sequences. Update the implementation to accumulate
output in a `[UInt8]` buffer during the state-machine pass, then convert it to
`Data` once at the end, preserving the existing `state` handling and `ESC
k`/`ST` title stripping behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 790a5fad-06be-4029-ad4c-7851a29fa940

📥 Commits

Reviewing files that changed from the base of the PR and between 05c03cf and 76891c3.

📒 Files selected for processing (4)
  • Sources/RemoteTmuxScreenTitleFilter.swift
  • Sources/RemoteTmuxSessionMirror.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/RemoteTmuxScreenTitleFilterTests.swift

Comment thread cmux.xcodeproj/project.pbxproj Outdated
Comment thread cmuxTests/RemoteTmuxScreenTitleFilterTests.swift Outdated
Comment thread Sources/RemoteTmuxScreenTitleFilter.swift Outdated
@ejc3
ejc3 force-pushed the ejc3/remote-tmux-screen-title branch from 76891c3 to d95b17a Compare June 28, 2026 23:42
ejc3 added a commit to ejc3/cmux that referenced this pull request Jun 29, 2026
…ew depends on it

# Conflicts:
#	Sources/RemoteTmuxSessionMirror.swift
@ejc3
ejc3 force-pushed the ejc3/remote-tmux-screen-title branch 2 times, most recently from 125af6e to 251054d Compare June 29, 2026 23:12
@vercel

vercel Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 1, 2026 9:50pm

ejc3 and others added 3 commits June 30, 2026 22:23
…ror output

Implement the byte-stream filter in the CmuxRemoteSession package (pure logic,
no app-lifecycle deps) rather than the app target root, matching the established
pattern for lifted remote-session logic. Build into a [UInt8] buffer, and assert
on raw Data in tests so a byte-corruption regression fails fast.
@ejc3
ejc3 force-pushed the ejc3/remote-tmux-screen-title branch from 55ebddb to 537ebc8 Compare July 1, 2026 05:29
@austinywang
austinywang merged commit 1a88c17 into manaflow-ai:main Jul 1, 2026
34 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 537ebc8f Deployed Jul 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants