Skip to content

feat: cmd+click file paths to markdown panel with tmux CC native pane support - #1517

Open
Tim-Feng wants to merge 22 commits into
manaflow-ai:mainfrom
Tim-Feng:pr/route-file-links-to-markdown-panel
Open

Tim-Feng wants to merge 22 commits into
manaflow-ai:mainfrom
Tim-Feng:pr/route-file-links-to-markdown-panel

Conversation

@Tim-Feng

@Tim-Feng Tim-Feng commented Mar 16, 2026 •

Copy link
Copy Markdown

Summary

  • cmd+click file paths open in the right-side markdown panel (local file path detection + validation)
  • tmux control mode bridge enables cmd+click inside tmux -CC sessions
  • tmux native pane rendering maps each tmux pane to a native Ghostty surface with full interaction support

Changes by phase

Phase 0: cmd+click → markdown panel

  • resolveTerminalOpenURLTarget() detects local file paths
  • Validates file exists and is not a directory; falls back to system handler
  • CodeRabbit review feedback fixes

Phase 1: tmux control mode bridge (Ghostty fork)

  • tmux_state action + ghostty_surface_tmux_pane_text() query API
  • VT parser: DCS passthrough fix for bytes 0x80-0x9F
  • tmux control.zig: %output octal unescape

Phase 2: native pane rendering

  • Shell wrapper auto-injects -CC (precmd hook; fixes leaked $TMUX env var)
  • Each tmux pane → ghostty_surface_new() with Manual I/O mode
  • %output routing to pane surface via processOutput()
  • Keyboard input: send-keys literal/key-name classification, SS3 arrow keys
  • IME: preedit redirect, bar cursor, full Chinese input flow
  • Paste: reads NSPasteboard directly + send-keys -l (multi-line safe: splits by newline + Enter)
  • Mouse: mouseDown/mouseMoved/mouseUp/mouseDragged routed to pane surface
  • cmd+click: shares host callback context (config.userdata) so OPEN_URL actions route correctly
  • Text selection: mouseDragged routing enables drag-to-select and copy
  • VT parser: absorbs ESC k ... ST (tmux/screen set-title) to prevent title text leaking to screen
  • activeTmuxPane helper centralizes pane routing logic

Ghostty fork changes (tmux-control-mode-bridge branch)

  • parse_table.zig: ESC k → sos_pm_apc_string
  • Parser.zig: tmux_dcs override for 0xA0-0xFF bytes + ESC k test
  • control.zig: unescapeTmuxOctal() + tests
  • stream_handler.zig: %output routing, pane register/unregister
  • embedded.zig: ghostty_surface_tmux_send_keys(), pane query/register APIs
  • Termio.zig: replaceTerminal()
  • Terminal.zig: clone() + restoreScreenState()
  • generic.zig + State.zig: preedit bar cursor rendering

Known issues

  • Initial sync disabled: Terminal.clone() viewport position is wrong; pane starts empty (user must press Enter to see prompt)
  • tmux CC failure recovery: attaching to a non-existent session freezes the terminal
  • Multi-pane routing: activeTmuxPane currently returns only the first pane

Test plan

  • Normal terminal cmd+click file path → right-side markdown panel
  • tmux CC mode cmd+click → right-side markdown panel
  • Typing, Up/Down arrow, cmd+V paste (single-line and multi-line)
  • Drag-to-select and copy
  • Chinese IME input
  • echo /path/to/file output displays correctly (with spaces)
  • Parser unit test: ESC k absorption

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added tmux integration with pane virtualization, control mode support, and unified input routing across tmux panes
    • Added cmd+click file opening in tmux with intelligent path detection and internal markdown panel support for local files
    • Added automatic tmux flag injection in shell integration (bash and zsh) for seamless tmux attach/new commands
    • Enhanced IME behavior for better text input handling in tmux environments

When cmd+clicking an absolute file path or file:// URL in the terminal,
open it in a side-by-side markdown panel instead of the system default
editor. This leverages the existing MarkdownPanel with live file watching.

Changes:
- Add .internalFile case to TerminalOpenURLTarget
- Route absolute paths and file:// URLs to .internalFile in
  resolveTerminalOpenURLTarget()
- Handle .internalFile in link click handler by opening a
  newMarkdownSplit beside the source terminal panel
- Fall back to NSWorkspace.shared.open() if workspace lookup fails

Closes manaflow-ai#1283
@vercel

vercel Bot commented Mar 16, 2026

Copy link
Copy Markdown

@Tim-Feng is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps greptile-apps Bot 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 16, 2026 •

Copy link
Copy Markdown

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 tmux control-mode surfaces and per-pane input routing to GhosttyNSView/GhosttyTerminalView, extends ghostty C API for tmux/pane/preedit I/O, treats absolute/file URLs as internal files (opening them in markdown panels when possible), and installs shell wrappers to auto-inject tmux -CC for interactive sessions.

Changes

Cohort / File(s) Summary
Terminal URL resolution & open handling
Sources/GhosttyTerminalView.swift
Added TerminalOpenURLTarget.internalFile(URL) and updated resolveTerminalOpenURLTarget(_:); GHOSTTY_ACTION_OPEN_URL now routes local files to internal markdown-panel flow with validation and NSWorkspace fallback; added DEBUG traces for link/mouse-over actions.
Tmux control-mode & pane surfaces
Sources/GhosttyNSView.swift, Sources/GhosttyTerminalView.swift
Implemented tmux control-mode state, pane registration/unregistration, per-pane Ghostty surfaces and write contexts, pane container view, lifecycle handlers, input routing (keyboard, paste, IME, mouse) to active pane surfaces, cmd+click path detection using pane text, and cleanup logic.
Ghostty C API & submodule
ghostty.h, ghostty (submodule pointer)
Added IO mode, manual write callback, preedit-with-cursor, tmux action tags/structs, pane register/unregister/send_keys/panes APIs, and surface_process_output; updated ghostty submodule commit.
Shell integration wrappers
Resources/shell-integration/cmux-bash-integration.bash, Resources/shell-integration/cmux-zsh-integration.zsh
Added tmux() bash wrapper and zsh precmd installer that define/override tmux() to auto-inject -CC for attach/new-like commands when CMUX socket is present (with opt-outs/guards).
Docs & reviews
docs/tmux-integration-plan.md, docs/phase2-review.md, docs/phase2-step1-4-review.md
Added phased tmux integration plan and two detailed reviews documenting safety concerns, unresolved bootstrap/unregister handshakes, and recommended follow-ups.
Utilities & scripts
scripts/reload.sh
Unset TMUX and TMUX_PANE in cleaned environment before launching the app.
Other
Resources/..., logging
Added DEBUG tracing for tmux action tags and mouse-over link logging; minor logging and plumbing tweaks.

Sequence Diagram

sequenceDiagram
    participant Terminal as PTY / tmux
    participant Ghostty as Ghostty surface
    participant NSView as GhosttyNSView / GhosttyTerminalView
    participant App as cmux App
    participant Workspace as Workspace / Panel Manager
    participant System as NSWorkspace

    Terminal->>Ghostty: tmux pane output / windows_changed / link metadata
    Ghostty->>NSView: emit action (GHOSTTY_ACTION_TMUX_WINDOWS_CHANGED / %output / OPEN_URL)
    NSView->>NSView: register/create per-pane surfaces & update pane state
    NSView->>App: resolveTerminalOpenURLTarget(path/url)
    alt internal file
        App->>Workspace: attempt newMarkdownSplit(using source workspace/panel IDs)
        Workspace-->>App: success or failure
        alt opened
            Workspace->>NSView: render file in markdown panel
        else fallback
            App->>System: NSWorkspace.shared.open(fileURL)
        end
    else external URL
        App->>System: NSWorkspace.shared.open(URL)
    end
    Note right of NSView: Input routing (keyboard/mouse/IME/paste) -> active pane surface via ghostty_surface_tmux_send_keys / preedit APIs
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 In panes where tmux and ghostty meet,
I hop on bytes and make the pathways neat.
I sniff a path, then nudge it to a view—
Markdown opens, small and bright and new. ✨📝

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers Summary, Phase breakdown, Known issues, and Test plan, but lacks Testing section with manual verification steps and demo video. Add a dedicated Testing section documenting manual verification steps and include a demo video link per the template requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: cmd+click file path opening in markdown panel combined with tmux control mode native pane support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2339-2354: The code currently discards the optional result of
resolved.workspace.newMarkdownSplit(...) and always returns true; change it to
check the returned MarkdownPanel? from newMarkdownSplit in performOnMain and if
it is nil call NSWorkspace.shared.open(URL(fileURLWithPath: path)) and return
its appropriate boolean (or true after opening) so failures fall back to opening
the file; update the block around performOnMain, AppDelegate.shared,
workspaceContainingPanel, and newMarkdownSplit to mirror the
nil-check/error-handling used in TerminalController.swift (check the optional
result and handle the nil branch by calling NSWorkspace.shared.open(...)).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fba12e8b-81d3-4744-b378-8a1d2e90158d

📥 Commits

Reviewing files that changed from the base of the PR and between 3b507d3 and 933eee1.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Comment thread Sources/GhosttyTerminalView.swift

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:2348">
P2: Validate that the path exists, is a readable file (not a directory) before opening a markdown split. Otherwise cmd‑clicking a missing path now opens a “File unavailable” markdown panel instead of falling back to the system handler.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift Outdated
…own panel

Addresses review feedback: cmd+clicking a missing or directory path now
falls back to the system handler instead of showing "File unavailable"
in a markdown panel.

@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.

♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

2360-2365: ⚠️ Potential issue | 🟠 Major

Handle markdown split creation failure before returning success.

On Line 2360, the optional return from resolved.workspace.newMarkdownSplit(...) is ignored, and the handler returns success unconditionally. If split creation fails, this branch silently swallows the click instead of falling back to NSWorkspace.shared.open(...).

Suggested fix
-                    _ = resolved.workspace.newMarkdownSplit(
-                        from: sourcePanelId,
-                        orientation: .horizontal,
-                        filePath: path
-                    )
-                    return true
+                    if resolved.workspace.newMarkdownSplit(
+                        from: sourcePanelId,
+                        orientation: .horizontal,
+                        filePath: path
+                    ) != nil {
+                        return true
+                    }
+                    return NSWorkspace.shared.open(URL(fileURLWithPath: path))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2360 - 2365, The current
branch calls resolved.workspace.newMarkdownSplit(from:orientation:filePath:) and
ignores its optional result, always returning true; update the handler to check
the returned optional from newMarkdownSplit (the call in GhosttyTerminalView
where newMarkdownSplit is invoked), and if it returns non-nil return true, but
if it returns nil then fall back to opening the file via
NSWorkspace.shared.open(URL(fileURLWithPath: path)) (or the existing fallback
used elsewhere) and return the outcome accordingly so failures don't silently
swallow the click.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2360-2365: The current branch calls
resolved.workspace.newMarkdownSplit(from:orientation:filePath:) and ignores its
optional result, always returning true; update the handler to check the returned
optional from newMarkdownSplit (the call in GhosttyTerminalView where
newMarkdownSplit is invoked), and if it returns non-nil return true, but if it
returns nil then fall back to opening the file via
NSWorkspace.shared.open(URL(fileURLWithPath: path)) (or the existing fallback
used elsewhere) and return the outcome accordingly so failures don't silently
swallow the click.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 164cd981-0f31-4045-98dc-f8d4a933329d

📥 Commits

Reviewing files that changed from the base of the PR and between 933eee1 and b45ca48.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Addresses CodeRabbit review: if newMarkdownSplit returns nil, open the
file via NSWorkspace instead of silently swallowing the click.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)

2319-2326: ⚠️ Potential issue | 🟠 Major

Handle .internalFile before the browser-link preference check.

Lines 2319-2326 return to NSWorkspace.shared.open(...) for every target when openTerminalLinksInCmuxBrowser() is off, so the new markdown-panel flow never runs for file-path clicks. Users who disabled in-app browser links keep the old external-editor behavior for the exact path this PR is trying to change.

🔧 Suggested fix
-            if !BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser() {
+            if case .internalFile = target {
+                // Keep going: local-file routing should not depend on the browser-link setting.
+            } else if !BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser() {
                 `#if` DEBUG
                 dlog("link.openURL cmuxBrowser=disabled, opening externally url=\(target.url)")
                 `#endif`
                 return performOnMain {
                     NSWorkspace.shared.open(target.url)
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2319 - 2326, Handle targets
with .internalFile before checking
BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(): move or add a guard
that detects when target.type == .internalFile (or equivalent enum/case) and
routes those to the new markdown-panel/internal-file flow instead of returning
early to NSWorkspace.shared.open; specifically update the logic around
BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(), target.url,
performOnMain and NSWorkspace.shared.open so that .internalFile targets bypass
the "openTerminalLinksInCmuxBrowser() == false" early-return and continue into
the markdown-panel handling code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 364-371: The enum case .internalFile should store a full URL
(change case internalFile(String) to internalFile(URL)) and the url computed
property should return that URL directly (similar to .embeddedBrowser and
.external), not construct a local file URL from a path; update any
parsing/factory logic that currently classifies file:// URLs to only produce
.internalFile for file URLs whose host is nil/empty or "localhost" and route
file URLs with a non-local host to .external(url); also update call sites that
previously expected a String path to use the stored URL and, where a filesystem
path is needed, use url.path.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2319-2326: Handle targets with .internalFile before checking
BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(): move or add a guard
that detects when target.type == .internalFile (or equivalent enum/case) and
routes those to the new markdown-panel/internal-file flow instead of returning
early to NSWorkspace.shared.open; specifically update the logic around
BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(), target.url,
performOnMain and NSWorkspace.shared.open so that .internalFile targets bypass
the "openTerminalLinksInCmuxBrowser() == false" early-return and continue into
the markdown-panel handling code path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16cd50b4-fba4-4487-bb49-e2927fb9f263

📥 Commits

Reviewing files that changed from the base of the PR and between b45ca48 and aa52010.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
1. Store full URL in .internalFile instead of String path
   - Preserves host info for network file:// URLs
   - Only treats local file:// (nil/empty/localhost host) as internal
   - Remote file:// URLs route to .external

2. Bypass browser-link preference for .internalFile targets
   - Users who disabled in-app browser links still get markdown panel
     for local file paths (this is file routing, not browser routing)
Tim-Feng and others added 7 commits March 17, 2026 19:15
Phase 1: minimal bridge (expose tmux viewer events to C API)
Phase 2: full tmux GUI integration (pane-to-surface mapping)

Key finding: Ghostty already has 4245 lines of tmux control mode
implementation, but events are not exposed through ghostty.h.
The .windows action in stream_handler.zig is TODO.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up tmux control mode events from Ghostty to cmux Swift layer
and add a pane text query API for link detection under tmux.

Ghostty submodule (7 files, +137 lines):
- tmux_state action (enter/exit) fired on control mode lifecycle
- ghostty_surface_tmux_pane_text(surface, pane_id, result) query API
- VT parser fix: tmux_dcs flag keeps DCS 1000p passthrough alive
  despite raw ESC/UTF-8 bytes in %output data (tmux-specific, does
  not affect XTGETTCAP/DECRQSS)
- Control parser: skip stray bytes in idle state (tmux 3.6+)
- ABI sync: add missing GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD

C header ghostty.h:
- GHOSTTY_ACTION_TMUX_STATE enum + action union entry
- ghostty_action_tmux_state_e (ENTER/EXIT)
- GHOSTTY_TMUX_PANE_ID_ANY convenience constant
- ghostty_surface_tmux_pane_text() declaration

Swift GhosttyTerminalView.swift:
- GhosttyNSView.tmuxControlMode property
- GHOSTTY_ACTION_TMUX_STATE handler
- Debug: immediate + delayed pane text query on enter

Verified: tmux -CC new-session in cmux DEV, session stable,
pane text query returns viewport content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add handleTmuxCmdClick() and openTmuxClickedPath() to intercept
cmd+click in tmux control mode, query pane text via the bridge API,
and open matched file paths in the markdown panel.

Also updates ghostty submodule (dumpString unwrap=false for accurate
row-based line mapping).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each tmux pane now renders as a native Ghostty surface with interactive
keyboard input. The host surface's I/O thread routes tmux %output to
registered pane surfaces, and keyboard events are forwarded through
Manual I/O → send-keys back to tmux.

Key changes:
- Swift: handleTmuxWindowsChanged creates Manual I/O surfaces per pane,
  with io_write_cb classifying input as literal text or tmux key names
- Swift: keyDown redirects to pane surface when tmuxControlMode is active,
  preserving full IME/interpretKeyEvents flow
- C header: add tmux pane query, register/unregister, send-keys APIs
- Ghostty submodule: %output routing, VT parser UTF-8 fix, octal unescape

Known limitations (follow-up):
- IME preedit not visible on pane surface (blind typing works)
- Mouse selection not forwarded to pane surface
- Initial sync is text-only MVP (no colors/cursor/modes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add inputSurface property: returns pane surface in tmux mode so
  keyboard input, IME preedit, and IME cursor positioning all target
  the correct surface
- Redirect syncPreedit to pane surface via inputSurface
- Redirect firstRect(forCharacterRange:) to pane view coordinate
  space so IME candidate window appears at correct position
- Commit preedit text as literal input when switching input methods
  mid-composition (captures keyboard layout change during marked text)
- Track markedTextCursorOffset from setMarkedText selectedRange
- Use guard var for surface in keyDown to allow tmux mode redirection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update ghostty submodule: Terminal.clone, replaceTerminal, preedit
  cursor bar rendering
- Swift: syncPreedit uses ghostty_surface_preedit_with_cursor to pass
  IME cursor offset for bar cursor display
- C header: add ghostty_surface_preedit_with_cursor declaration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add tmux() wrapper function to both zsh and bash shell integration
scripts. When the user runs `tmux attach`, `tmux new-session`, or
bare `tmux` in an interactive cmux terminal, the wrapper automatically
inserts -CC to enable native pane rendering via tmux control mode.

Non-client commands (ls, send-keys, kill-server, etc.) pass through
unchanged. Global flags that consume a value (-L, -S, -f, -c) are
properly skipped during subcommand detection. The `a` alias for
`attach` is also recognized.

Escape hatches:
- `command tmux ...` bypasses the wrapper
- `CMUX_TMUX_AUTO_CC=0` disables globally
- Already inside tmux ($TMUX set) → no rewrite
- Already has -CC/-C → no rewrite
- Non-interactive or non-tty → no rewrite

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mrgoonie

Copy link
Copy Markdown

hmm... this PR looks good

Tim-Feng and others added 4 commits March 19, 2026 22:06
Phase 2 tmux pane rendering improvements:

- Fix $TMUX env leak: cmux terminals inherited $TMUX from the launcher,
  causing the shell wrapper to skip -CC injection. Now check
  CMUX_SOCKET_PATH to distinguish cmux terminals from real tmux sessions.
  Also unset TMUX/TMUX_PANE in reload.sh OPEN_CLEAN_ENV.

- cmd+click opens files: share host callback context with pane surface
  (config.userdata) so OPEN_URL actions route to the Swift handler.
  Mouse events (mouseDown/mouseMoved/mouseUp) route to pane surface
  for native link detection.

- Paste in tmux mode: bypass Ghostty's async clipboard flow (which
  sends results to host surface due to shared userdata). Read
  NSPasteboard directly and send via send-keys -l. Multi-line safe:
  split by newlines, send Enter between lines.

- Arrow keys: support SS3/application mode (ESC O A/B/C/D) in addition
  to CSI mode (ESC [ A/B/C/D).

- First-key guard: drop key events when tmuxControlMode=true but pane
  surfaces aren't ready yet, preventing keys from going to host.

- Space key: send as tmux key name (send-keys Space) instead of
  send-keys -l to avoid quoting issues.

- Shell wrapper: use precmd hook to install tmux() function after
  .zshrc (avoids oh-my-zsh override).

- Refactor: extract activeTmuxPane helper to centralize pane routing
  (currently returns first pane, ready for multi-pane support).

Known issues:
- Pane surface space rendering: tmux pane has correct content but
  pane surface display drops spaces (terminal state context issue)
- Initial sync disabled: Terminal.clone viewport position wrong
- Multi-pane routing: activeTmuxPane returns first pane only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ection

Without this, drag events go to the host surface and text selection
in tmux pane surfaces doesn't work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dering

Fixes tmux pane surface displaying leaked title text (e.g. "echohello
world" instead of "hello world"). The ESC k ... ST sequence from
tmux/screen is now silently absorbed by the VT parser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Tim-Feng Tim-Feng changed the title feat: route local file path clicks to markdown panel feat: cmd+click file paths → markdown panel (with tmux CC native pane) Mar 20, 2026
@Tim-Feng Tim-Feng changed the title feat: cmd+click file paths → markdown panel (with tmux CC native pane) feat: cmd+click file paths to markdown panel with tmux CC native pane support Mar 20, 2026

@cubic-dev-ai cubic-dev-ai 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.

4 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Resources/shell-integration/cmux-bash-integration.bash">

<violation number="1" location="Resources/shell-integration/cmux-bash-integration.bash:535">
P2: tmux subcommand detection misses `-T` (a value-taking global flag), which can misparse arguments and break `-CC` auto-injection.</violation>
</file>

<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:4208">
P1: Tmux input routing uses `tmuxPaneSurfaces.values.first` as the active pane, so in multi-pane tmux sessions keyboard/paste/mouse/IME can target the wrong pane.</violation>

<violation number="2" location="Sources/GhosttyTerminalView.swift:5796">
P2: Pane state is removed before unregister ACK, so ACK handler cannot free pane surface/write context, causing leaks on pane removal.</violation>
</file>

<file name="Resources/shell-integration/cmux-zsh-integration.zsh">

<violation number="1" location="Resources/shell-integration/cmux-zsh-integration.zsh:591">
P2: tmux subcommand detection misses valid global options with arguments (e.g. `-T`), causing auto `-CC` injection to be skipped for attach/new flows.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift
for arg in "$@"; do
if (( skip_next )); then skip_next=0; continue; fi
case "$arg" in
-L|-S|-f|-c) skip_next=1; continue ;; # these flags consume the next arg

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: tmux subcommand detection misses -T (a value-taking global flag), which can misparse arguments and break -CC auto-injection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/shell-integration/cmux-bash-integration.bash, line 535:

<comment>tmux subcommand detection misses `-T` (a value-taking global flag), which can misparse arguments and break `-CC` auto-injection.</comment>

<file context>
@@ -509,4 +509,41 @@ _cmux_fix_path() {
+    for arg in "$@"; do
+        if (( skip_next )); then skip_next=0; continue; fi
+        case "$arg" in
+            -L|-S|-f|-c) skip_next=1; continue ;;  # these flags consume the next arg
+            -*) continue ;;
+            *) subcmd="$arg"; break ;;
</file context>
Suggested change
-L|-S|-f|-c) skip_next=1; continue ;; # these flags consume the next arg
-L|-S|-f|-c|-T) skip_next=1; continue ;; # these flags consume the next arg
Fix with Cubic

tmuxPendingUnregister[paneId] = state.regId
ghostty_surface_tmux_unregister_pane(hostSurface, paneId, state.regId)
state.view.removeFromSuperview()
tmuxPaneSurfaces.removeValue(forKey: paneId)

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Pane state is removed before unregister ACK, so ACK handler cannot free pane surface/write context, causing leaks on pane removal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 5796:

<comment>Pane state is removed before unregister ACK, so ACK handler cannot free pane surface/write context, causing leaks on pane removal.</comment>

<file context>
@@ -5454,16 +5601,526 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
+            tmuxPendingUnregister[paneId] = state.regId
+            ghostty_surface_tmux_unregister_pane(hostSurface, paneId, state.regId)
+            state.view.removeFromSuperview()
+            tmuxPaneSurfaces.removeValue(forKey: paneId)
+            #if DEBUG
+            dlog("tmux.pane remove pane=\(paneId) regId=\(state.regId)")
</file context>
Fix with Cubic

for arg in "$@"; do
if (( skip_next )); then skip_next=0; continue; fi
case "$arg" in
-L|-S|-f|-c) skip_next=1; continue ;;

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: tmux subcommand detection misses valid global options with arguments (e.g. -T), causing auto -CC injection to be skipped for attach/new flows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/shell-integration/cmux-zsh-integration.zsh, line 591:

<comment>tmux subcommand detection misses valid global options with arguments (e.g. `-T`), causing auto `-CC` injection to be skipped for attach/new flows.</comment>

<file context>
@@ -567,8 +567,46 @@ _cmux_zshexit() {
+        for arg in "$@"; do
+            if (( skip_next )); then skip_next=0; continue; fi
+            case "$arg" in
+                -L|-S|-f|-c) skip_next=1; continue ;;
+                -*) continue ;;
+                *) subcmd="$arg"; break ;;
</file context>
Suggested change
-L|-S|-f|-c) skip_next=1; continue ;;
-L|-S|-f|-c|-T) skip_next=1; continue ;;
Fix with Cubic

@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: 7

🧹 Nitpick comments (1)
docs/phase2-review.md (1)

37-37: Refactor: Use unique subheadings to improve navigation.

The document uses repeated "## Verdict", "## Recommendation", and "## Bottom line" subheadings within each main section. While this provides structural consistency, it creates ambiguous references and markdown linting warnings (MD024).

Consider using unique subheadings that include the topic context, or use level-3 headings (###) for the repeated substructure:

-## Verdict
+### Verdict: Thread-safe but lifetime risk

This applies to all sections (lines 37, 65, 84, 93, 121, 140, 148, 171, 189, 199).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/phase2-review.md` at line 37, The document repeats identical
second-level headings ("## Verdict", "## Recommendation", "## Bottom line")
across multiple sections causing ambiguous references and MD024 warnings; update
each repeated heading to be unique by appending the section context (e.g., "##
Verdict — [Section Topic]" or switch to third-level headings like "### Verdict:
[Section Topic]") for all occurrences noted (lines referenced in comment) so
each subheading is distinct and navigable while preserving the original content
under functions or sections named by their current headings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghostty`:
- Line 1: The parent repo's submodule pointer references commit
8408eec2a7ca4aa0887c071e91e75d8d845b3021 which is not present on the fork's
remote; push that commit to the fork's origin main branch (manaflow-ai/ghostty)
so the commit becomes reachable, then update the submodule pointer in the parent
repo to the pushed commit SHA and commit that change; finally update
docs/ghostty-fork.md with any notes about the fork, merge/conflict details, and
the fact that this commit was pushed to the fork before bumping the submodule
pointer.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 5790-5799: The loop currently removes entries from
tmuxPaneSurfaces and only stores regId in tmuxPendingUnregister, which causes
handleTmuxPaneUnregistered(...) to lack the surface and writeContext to free;
instead, when scheduling an unregister in the for loop (where
ghostty_surface_tmux_unregister_pane is called), move the entire state object
into tmuxPendingUnregister[paneId] (preserve state.regId, view, writeContext,
etc.), do NOT call state.view.removeFromSuperview() nor
tmuxPaneSurfaces.removeValue(forKey:) there, and let
handleTmuxPaneUnregistered(paneId, ...) own removing the view, freeing
writeContext and removing from tmuxPaneSurfaces once the ack arrives; update any
code that expects tmuxPendingUnregister to contain only regId to handle the full
state structure.
- Around line 5969-5975: The pane view frame is updated but the underlying
ghostty_surface_t isn’t resized, so after computing the new CGRect via
tmuxPaneFrame(paneInfo) and assigning it to state.view.frame, also compute the
new pixel dimensions from state.view.bounds.size multiplied by the display scale
(e.g. state.view.window?.screen.scale or UIScreen.main.scale) and call
ghostty_surface_set_size(...) on the surface associated with that pane (use the
same tmuxPaneSurfaces[paneInfo.pane_id] entry — e.g. state.surface or
equivalent) to set the pixel width/height; only call the resize when the pixel
size actually changes to avoid unnecessary I/O.
- Around line 5802-5810: tmuxPaneContainer and its overlay NSViews are blocking
pointer events to GhosttyNSView (so GhosttyNSView.mouseDown/mouseMoved etc.
never run); change the container/overlay creation (tmuxPaneContainer and the
views added around lines where GhosttyPane container is created) to be
pointer-transparent by using the same passthrough pattern as
GhosttyFlashOverlayView (override hitTest to return nil) or otherwise forward
events to underlying views so tmux routing logic still receives mouse events;
update the custom overlay view class (or subclass the container) and use that
when instantiating tmuxPaneContainer and the other overlay views referenced in
the nearby block so they don’t intercept hit-testing.
- Around line 4202-4218: activeTmuxPane currently returns
tmuxPaneSurfaces.values.first which incorrectly maps inputSurface to an
arbitrary pane; update the code to track and resolve an explicit active pane ID
(e.g., store a tmuxActivePaneID property) and have activeTmuxPane fetch
tmuxPaneSurfaces[tmuxActivePaneID] (or resolve the pane from the event/mouse
location when appropriate). Ensure handleTmuxWindowsChanged, mouse/focus/hover
handlers, and any paste/IME entry points update tmuxActivePaneID (or compute
from event coordinates) so inputSurface always returns the focused/hovered pane
surface instead of values.first. Use the existing symbols activeTmuxPane,
inputSurface, tmuxPaneSurfaces, and handleTmuxWindowsChanged when making these
changes.
- Around line 4481-4513: The tmux branch currently only reads
NSPasteboard.general.string(forType: .string) which loses rich-text, file-URL
and image-only paste behaviors handled by GhosttyPasteboardHelper; update the
activeTmuxPane branch (around activeTmuxPane, ghostty_surface_tmux_send_keys,
and the fallback performBindingAction("paste_from_clipboard")) to obtain the
clipboard payload via the existing GhosttyPasteboardHelper API (use its method
that returns the best pasteable string(s) or file/url fallback instead of raw
.string), then keep the existing line-splitting/send-keys loop so file URLs and
rich-text fallbacks are preserved for tmux pastes. Ensure empty checks and early
return behavior remain the same.
- Around line 5641-5652: The code is creating fullText with String(cString: ptr)
which assumes a NUL-terminated C string; instead use the returned length field
on ghostty_text_s to build Data and decode UTF-8. Replace the String(cString:
ptr) usage in the ghostty_surface_tmux_pane_text handling (the textResult /
ghostty_text_s block) with Data(bytes: ptr, count: Int(textResult.text_len)) and
then String(decoding: thatData, as: UTF8.self) to produce fullText, keeping the
existing defer that calls ghostty_surface_free_text(surface, &mutable).

---

Nitpick comments:
In `@docs/phase2-review.md`:
- Line 37: The document repeats identical second-level headings ("## Verdict",
"## Recommendation", "## Bottom line") across multiple sections causing
ambiguous references and MD024 warnings; update each repeated heading to be
unique by appending the section context (e.g., "## Verdict — [Section Topic]" or
switch to third-level headings like "### Verdict: [Section Topic]") for all
occurrences noted (lines referenced in comment) so each subheading is distinct
and navigable while preserving the original content under functions or sections
named by their current headings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f06919b9-c6af-4359-aa17-71819db0db7b

📥 Commits

Reviewing files that changed from the base of the PR and between aa52010 and a31b8a3.

📒 Files selected for processing (9)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Sources/GhosttyTerminalView.swift
  • docs/phase2-review.md
  • docs/phase2-step1-4-review.md
  • docs/tmux-integration-plan.md
  • ghostty
  • ghostty.h
  • scripts/reload.sh
✅ Files skipped from review due to trivial changes (1)
  • docs/tmux-integration-plan.md

Comment thread ghostty Outdated
@@ -1 +1 @@
Subproject commit bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42
Subproject commit 8408eec2a7ca4aa0887c071e91e75d8d845b3021

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target_commit="8408eec2a7ca4aa0887c071e91e75d8d845b3021"

# Confirm current submodule state
git submodule status ghostty
git -C ghostty rev-parse HEAD

# Verify target commit is published on remote main
git -C ghostty fetch origin main --quiet
git -C ghostty merge-base --is-ancestor "$target_commit" origin/main

# Verify fork-maintenance docs mention/update for this bump
rg -n "8408eec2a7ca4aa0887c071e91e75d8d845b3021|8408eec2|ghostty-fork|conflict" docs/ghostty-fork.md

Repository: manaflow-ai/cmux

Length of output: 341


Push the ghostty commit to the fork's origin/main branch before updating the submodule pointer.

The commit 8408eec2a7ca4aa0887c071e91e75d8d845b3021 is not reachable on the remote fork, which will cause clone failures. Ensure this commit is pushed to manaflow-ai/ghostty main branch, then update the parent pointer. Also update docs/ghostty-fork.md with any fork/conflict notes for this change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghostty` at line 1, The parent repo's submodule pointer references commit
8408eec2a7ca4aa0887c071e91e75d8d845b3021 which is not present on the fork's
remote; push that commit to the fork's origin main branch (manaflow-ai/ghostty)
so the commit becomes reachable, then update the submodule pointer in the parent
repo to the pushed commit SHA and commit that change; finally update
docs/ghostty-fork.md with any notes about the fork, merge/conflict details, and
the fact that this commit was pushed to the fork before bumping the submodule
pointer.

Comment thread Sources/GhosttyTerminalView.swift
Comment on lines +4481 to 4513
// In tmux mode, Ghostty's async clipboard flow would send the result
// back to the host surface (shared userdata). Instead, read the
// clipboard directly and route through the pane's send-keys path.
if let paneState = activeTmuxPane {
guard let str = NSPasteboard.general.string(forType: .string),
!str.isEmpty else { return }
let hostSurface = paneState.writeContext.pointee.hostSurface
let paneId = paneState.writeContext.pointee.paneId
// Split by newlines to avoid raw newlines breaking the tmux
// command stream. Each line is sent as literal text, with
// explicit Enter key events between lines.
let lines = str.components(separatedBy: .newlines)
for (i, line) in lines.enumerated() {
if !line.isEmpty, let data = line.data(using: .utf8) {
data.withUnsafeBytes { rawBuf in
let ptr = rawBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
ghostty_surface_tmux_send_keys(hostSurface, paneId, ptr, data.count, 0)
}
}
// Send Enter between lines (not after the last line)
if i < lines.count - 1 {
"Enter".withCString { cstr in
ghostty_surface_tmux_send_keys(
hostSurface, paneId,
UnsafePointer<UInt8>(OpaquePointer(cstr)),
strlen(cstr), 1 // key_type=1 (key name)
)
}
}
}
return
}
_ = performBindingAction("paste_from_clipboard")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tmux paste drops the existing pasteboard decoding behavior.

This branch only reads .string, so file URLs, rich-text fallback text, and clipboard-only images stop pasting in tmux even though normal terminal paste supports them through GhosttyPasteboardHelper.

Possible fix
-            guard let str = NSPasteboard.general.string(forType: .string),
-                  !str.isEmpty else { return }
+            let pasteboard = NSPasteboard.general
+            let str = GhosttyPasteboardHelper.stringContents(from: pasteboard)
+                ?? GhosttyPasteboardHelper.saveClipboardImageIfNeeded(
+                    from: pasteboard,
+                    assumeNoText: true
+                )
+            guard let str, !str.isEmpty else { return }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 4481 - 4513, The tmux branch
currently only reads NSPasteboard.general.string(forType: .string) which loses
rich-text, file-URL and image-only paste behaviors handled by
GhosttyPasteboardHelper; update the activeTmuxPane branch (around
activeTmuxPane, ghostty_surface_tmux_send_keys, and the fallback
performBindingAction("paste_from_clipboard")) to obtain the clipboard payload
via the existing GhosttyPasteboardHelper API (use its method that returns the
best pasteable string(s) or file/url fallback instead of raw .string), then keep
the existing line-splitting/send-keys loop so file URLs and rich-text fallbacks
are preserved for tmux pastes. Ensure empty checks and early return behavior
remain the same.

Comment on lines +5641 to +5652
var textResult = ghostty_text_s()
guard ghostty_surface_tmux_pane_text(surface, UInt.max, &textResult),
let ptr = textResult.text else {
#if DEBUG
dlog("tmux.cmdClick pane text query failed")
#endif
return false
}
defer { var mutable = textResult; ghostty_surface_free_text(surface, &mutable) }

let fullText = String(cString: ptr)
let lines = fullText.components(separatedBy: "\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the ghostty_text_s contract and compare local decoding sites.
mapfile -t headers < <(fd 'ghostty\.h$' || true)
for header in "${headers[@]}"; do
  printf '=== %s ===\n' "$header"
  sed -n '/typedef struct ghostty_text_s/,/} ghostty_text_s;/p' "$header"
done

sed -n '4597,4608p;5641,5652p' Sources/GhosttyTerminalView.swift

Repository: manaflow-ai/cmux

Length of output: 116


🏁 Script executed:

# Find ghostty.h header file
fd 'ghostty\.h$'

Repository: manaflow-ai/cmux

Length of output: 69


🏁 Script executed:

# Once we have the header location, examine the ghostty_text_s struct
# Also look at the code in GhosttyTerminalView.swift at the mentioned lines
rg 'typedef struct ghostty_text_s' -A 10

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check the actual code at lines 5641-5652
sed -n '5641,5652p' Sources/GhosttyTerminalView.swift

Repository: manaflow-ai/cmux

Length of output: 552


🏁 Script executed:

# Search for other uses of ghostty_text_s to see decode patterns
rg 'ghostty_text_s' Sources/GhosttyTerminalView.swift -B 2 -A 5 | head -100

Repository: manaflow-ai/cmux

Length of output: 1013


🏁 Script executed:

# Read the ghostty.h file to see ghostty_text_s struct definition
cat ghostty.h | grep -A 10 'typedef struct ghostty_text_s'

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for proper usage of text_len with ghostty_text_s decoding
rg 'text\.text_len|textResult\.text_len' -B 3 -A 3

Repository: manaflow-ai/cmux

Length of output: 1138


🏁 Script executed:

# Look for String decoding patterns with ghostty_text_s in the file
rg 'String.*text\.text\)|Data.*text\.text' Sources/GhosttyTerminalView.swift -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 301


Decode pane text with text_len, not String(cString:).

The code at lines 5641-5652 uses String(cString: ptr) which assumes NUL termination. Other decode sites in the codebase (GhosttyTerminalView.swift and TerminalController.swift) consistently use Data(bytes: ptr, count: Int(text.text_len)) followed by String(decoding: _, as: UTF8.self). The text_len field must be used to avoid truncation or buffer overread.

Fix
-        let fullText = String(cString: ptr)
+        let fullText = String(
+            decoding: Data(bytes: ptr, count: Int(textResult.text_len)),
+            as: UTF8.self
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5641 - 5652, The code is
creating fullText with String(cString: ptr) which assumes a NUL-terminated C
string; instead use the returned length field on ghostty_text_s to build Data
and decode UTF-8. Replace the String(cString: ptr) usage in the
ghostty_surface_tmux_pane_text handling (the textResult / ghostty_text_s block)
with Data(bytes: ptr, count: Int(textResult.text_len)) and then String(decoding:
thatData, as: UTF8.self) to produce fullText, keeping the existing defer that
calls ghostty_surface_free_text(surface, &mutable).

Comment on lines +5790 to +5799
// Remove stale panes (two-phase: unregister first, destroy on ack)
for paneId in toRemove {
guard let state = tmuxPaneSurfaces[paneId] else { continue }
tmuxPendingUnregister[paneId] = state.regId
ghostty_surface_tmux_unregister_pane(hostSurface, paneId, state.regId)
state.view.removeFromSuperview()
tmuxPaneSurfaces.removeValue(forKey: paneId)
#if DEBUG
dlog("tmux.pane remove pane=\(paneId) regId=\(state.regId)")
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep removed panes alive until the unregister ack arrives.

Removing tmuxPaneSurfaces[paneId] here means handleTmuxPaneUnregistered(...) no longer has the surface or writeContext to free, so every topology change leaks them. Keep the full state in a pending-unregister structure and let the ack handler own destruction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5790 - 5799, The loop
currently removes entries from tmuxPaneSurfaces and only stores regId in
tmuxPendingUnregister, which causes handleTmuxPaneUnregistered(...) to lack the
surface and writeContext to free; instead, when scheduling an unregister in the
for loop (where ghostty_surface_tmux_unregister_pane is called), move the entire
state object into tmuxPendingUnregister[paneId] (preserve state.regId, view,
writeContext, etc.), do NOT call state.view.removeFromSuperview() nor
tmuxPaneSurfaces.removeValue(forKey:) there, and let
handleTmuxPaneUnregistered(paneId, ...) own removing the view, freeing
writeContext and removing from tmuxPaneSurfaces once the ack arrives; update any
code that expects tmuxPendingUnregister to contain only regId to handle the full
state structure.

Comment on lines +5802 to +5810
// Ensure container exists — add above the scroll view so it's
// not hidden behind the host terminal's Metal rendering layer.
if tmuxPaneContainer == nil {
let container = NSView(frame: bounds)
container.wantsLayer = true
container.autoresizingMask = [.width, .height]
// Walk up: GhosttyNSView → documentView → clipView → scrollView → GhosttySurfaceScrollView
if let parentView = enclosingScrollView?.superview {
parentView.addSubview(container, positioned: .above, relativeTo: nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the tmux overlay views pointer-transparent.

These views are inserted above the hosted terminal as plain NSViews. They become the topmost hit-test targets, so GhosttyNSView.mouseDown/mouseMoved/... will not see pointer events and the manual tmux routing below cannot run. Use a passthrough view here (for example the same hitTest -> nil pattern as GhosttyFlashOverlayView) or forward events explicitly.

Also applies to: 5841-5850

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5802 - 5810,
tmuxPaneContainer and its overlay NSViews are blocking pointer events to
GhosttyNSView (so GhosttyNSView.mouseDown/mouseMoved etc. never run); change the
container/overlay creation (tmuxPaneContainer and the views added around lines
where GhosttyPane container is created) to be pointer-transparent by using the
same passthrough pattern as GhosttyFlashOverlayView (override hitTest to return
nil) or otherwise forward events to underlying views so tmux routing logic still
receives mouse events; update the custom overlay view class (or subclass the
container) and use that when instantiating tmuxPaneContainer and the other
overlay views referenced in the nearby block so they don’t intercept
hit-testing.

Comment on lines +5969 to +5975
// Update layout for existing panes
for i in 0..<Int(written) {
let paneInfo = panes[i]
if let state = tmuxPaneSurfaces[paneInfo.pane_id], !toAdd.contains(paneInfo.pane_id) {
state.view.frame = tmuxPaneFrame(paneInfo)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resize the pane surface when the pane frame changes.

Updating only state.view.frame leaves the backing ghostty_surface_t at its old pixel size. These manual-I/O pane surfaces need the same ghostty_surface_set_size(...) maintenance as the normal terminal path, or rendering and hit-testing drift after tmux layout changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5969 - 5975, The pane view
frame is updated but the underlying ghostty_surface_t isn’t resized, so after
computing the new CGRect via tmuxPaneFrame(paneInfo) and assigning it to
state.view.frame, also compute the new pixel dimensions from
state.view.bounds.size multiplied by the display scale (e.g.
state.view.window?.screen.scale or UIScreen.main.scale) and call
ghostty_surface_set_size(...) on the surface associated with that pane (use the
same tmuxPaneSurfaces[paneInfo.pane_id] entry — e.g. state.surface or
equivalent) to set the pixel width/height; only call the resize when the pixel
size actually changes to avoid unnecessary I/O.

The tmux status bar cannot be exposed by leaving a gap in the pane
container. Ghostty's tmux control mode intercepts all output via the
viewer; the host terminal does not reliably render tmux chrome/status
bar as a separate visible layer.

Revert container frame to full parent bounds. A native tmux status
indicator can be added as a future enhancement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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: 1

♻️ Duplicate comments (6)
Sources/GhosttyTerminalView.swift (6)

4481-4511: ⚠️ Potential issue | 🟠 Major

Use GhosttyPasteboardHelper in the tmux paste path too.

This branch only reads .string, so file URLs, rich-text fallback text, and image-only clipboard content stop pasting in tmux even though the normal terminal path supports them.

Possible fix
-            guard let str = NSPasteboard.general.string(forType: .string),
-                  !str.isEmpty else { return }
+            let pasteboard = NSPasteboard.general
+            let str = GhosttyPasteboardHelper.stringContents(from: pasteboard)
+                ?? GhosttyPasteboardHelper.saveClipboardImageIfNeeded(
+                    from: pasteboard,
+                    assumeNoText: true
+                )
+            guard let str, !str.isEmpty else { return }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 4481 - 4511, The tmux branch
reads only NSPasteboard.general.string(forType: .string), which drops file URLs,
rich-text fallbacks and image-only clipboards; replace that direct read with the
existing GhosttyPasteboardHelper pasteboard-reading API (use
GhosttyPasteboardHelper to obtain the pasteboard content/string(s) and any
fallback text or image-derived text) before splitting into lines and sending via
ghostty_surface_tmux_send_keys; keep using activeTmuxPane, hostSurface, paneId
and the existing loop/Enter logic but source the text from
GhosttyPasteboardHelper so all clipboard types supported by the normal terminal
paste path also work in the tmux paste path.

5641-5652: ⚠️ Potential issue | 🔴 Critical

Decode tmux pane text with text_len, not String(cString:).

ghostty_text_s is length-delimited. Line 5651 assumes NUL termination, which can truncate pane content or read past the returned buffer.

Possible fix
-        let fullText = String(cString: ptr)
+        let fullText = String(
+            decoding: Data(bytes: ptr, count: Int(textResult.text_len)),
+            as: UTF8.self
+        )
#!/bin/bash
set -euo pipefail

fd 'ghostty.h$' -x sh -c '
  printf "=== %s ===\n" "$1"
  sed -n "/typedef struct ghostty_text_s/,/} ghostty_text_s;/p" "$1"
' sh {}

sed -n '4597,4608p;5641,5652p' Sources/GhosttyTerminalView.swift
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5641 - 5652, The code
incorrectly decodes the tmux pane buffer using String(cString:) which assumes
NUL-termination; instead read the length-delimited ghostty_text_s buffer using
its text_len field returned by ghostty_surface_tmux_pane_text and build the
String from the raw bytes (respecting text_len) before splitting into lines;
update the usage around ghostty_text_s, ghostty_surface_tmux_pane_text,
ghostty_surface_free_text and the fullText/lines construction so you create the
String from the pointer + text_len rather than String(cString:).

4202-4218: ⚠️ Potential issue | 🟠 Major

Stop routing inputSurface through tmuxPaneSurfaces.values.first.

This still picks an arbitrary pane. Once there is more than one tmux pane, keyboard, paste, IME, and hover/click routing can all land in the wrong surface. Track an explicit active pane ID and resolve inputSurface from that instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 4202 - 4218, Replace the
arbitrary selection of a tmux pane via tmuxPaneSurfaces.values.first used by
activeTmuxPane/inputSurface with an explicit tracked active pane identifier: add
a stored property (e.g., activeTmuxPaneID or activeTmuxPaneKey) that is updated
wherever pane focus/hover/click changes, then change activeTmuxPane to look up
tmuxPaneSurfaces[activeTmuxPaneID] when tmuxControlMode is true (falling back to
nil), and have inputSurface return that pane.surface when present; update any
event handlers that change focus to set the new active pane ID instead of
relying on values.first.

5967-5972: ⚠️ Potential issue | 🟠 Major

Resize the backing pane surface when the pane frame changes.

Updating only state.view.frame leaves the manual-I/O pane surface at its old pixel size. After tmux layout changes, rendering and hit-testing drift until the pane is recreated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5967 - 5972, When updating
existing panes in the loop (tmuxPaneSurfaces[paneInfo.pane_id]), also detect
when the view frame actually changed (compare state.view.frame to
tmuxPaneFrame(paneInfo)), set the new frame, and then resize the pane's backing
surface so its pixel buffer matches the new view size (e.g. call the
surface/resizing API on the state object after changing state.view.frame).
Ensure you reference tmuxPaneSurfaces, paneInfo.pane_id, state.view.frame and
tmuxPaneFrame(paneInfo) so the backing surface is updated whenever the layout
changes.

5802-5809: ⚠️ Potential issue | 🟠 Major

Make the tmux container and pane host views pointer-transparent.

These views sit above the terminal portal. As plain NSView hit-test targets, they intercept mouse events before GhosttyNSView.mouseDown/mouseMoved/... can run, which breaks the manual tmux routing below. Use a passthrough view here (hitTest -> nil) or forward events explicitly.

Also applies to: 5844-5848

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5802 - 5809, The container
and tmux pane host views (tmuxPaneContainer and the pane-host view created
later) are intercepting mouse events; make them pointer-transparent by using a
passthrough NSView implementation (e.g., a PassthroughView subclass whose
hitTest(_:) returns nil) instead of plain NSView so events fall through to
GhosttyNSView.mouseDown/mouseMoved/etc.; update the allocation sites where you
create `let container = NSView(frame: ...)` (and the corresponding pane-host
creation at the 5844–5848 block) to instantiate the passthrough view class and
ensure autoresizingMask and addSubview usage remain the same.

5790-5797: ⚠️ Potential issue | 🟠 Major

Don’t drop pane state before the unregister ack.

Removing tmuxPaneSurfaces[paneId] here leaves handleTmuxPaneUnregistered(...) with only a regId, so the surface and writeContext never get freed on topology changes. Move the full state into a pending-unregister map and let the ack handler own teardown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5790 - 5797, Currently you
remove the pane state from tmuxPaneSurfaces and only store regId in
tmuxPendingUnregister, which prevents handleTmuxPaneUnregistered from freeing
the surface and writeContext; instead, move the full state (not just
state.regId) into tmuxPendingUnregister[paneId], call
ghostty_surface_tmux_unregister_pane(hostSurface, paneId, state.regId) as
before, and do not call state.view.removeFromSuperview() or
tmuxPaneSurfaces.removeValue(forKey: paneId) here; update
handleTmuxPaneUnregistered to look up and tear down the full state from
tmuxPendingUnregister (removing the view, freeing writeContext, and clearing the
pending entry) so the ack handler owns the teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 5605-5620: The tmux branch currently returns immediately after
sending the mouse event to the pane surface, which prevents the cmd+click
file-path fallback (handleTmuxCmdClick) from running and the path-matcher from
capturing paths with spaces; update the mouseDown handling for activeTmuxPane
(paneState, paneSurface, ghostty_surface_mouse_pos,
ghostty_surface_mouse_button, modsFromEvent) so that after forwarding the mouse
event you only return if the pane surface handled a command-click link;
otherwise, detect a command-click (event.modifierFlags.contains(.command)) and
call handleTmuxCmdClick(...) as a fallback before returning; also replace the
current matcher logic used by handleTmuxCmdClick with a more permissive pattern
that allows spaces in file paths (or supports quoted/tilde paths) so paths like
"~/My File.md" are matched fully.

---

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4481-4511: The tmux branch reads only
NSPasteboard.general.string(forType: .string), which drops file URLs, rich-text
fallbacks and image-only clipboards; replace that direct read with the existing
GhosttyPasteboardHelper pasteboard-reading API (use GhosttyPasteboardHelper to
obtain the pasteboard content/string(s) and any fallback text or image-derived
text) before splitting into lines and sending via
ghostty_surface_tmux_send_keys; keep using activeTmuxPane, hostSurface, paneId
and the existing loop/Enter logic but source the text from
GhosttyPasteboardHelper so all clipboard types supported by the normal terminal
paste path also work in the tmux paste path.
- Around line 5641-5652: The code incorrectly decodes the tmux pane buffer using
String(cString:) which assumes NUL-termination; instead read the
length-delimited ghostty_text_s buffer using its text_len field returned by
ghostty_surface_tmux_pane_text and build the String from the raw bytes
(respecting text_len) before splitting into lines; update the usage around
ghostty_text_s, ghostty_surface_tmux_pane_text, ghostty_surface_free_text and
the fullText/lines construction so you create the String from the pointer +
text_len rather than String(cString:).
- Around line 4202-4218: Replace the arbitrary selection of a tmux pane via
tmuxPaneSurfaces.values.first used by activeTmuxPane/inputSurface with an
explicit tracked active pane identifier: add a stored property (e.g.,
activeTmuxPaneID or activeTmuxPaneKey) that is updated wherever pane
focus/hover/click changes, then change activeTmuxPane to look up
tmuxPaneSurfaces[activeTmuxPaneID] when tmuxControlMode is true (falling back to
nil), and have inputSurface return that pane.surface when present; update any
event handlers that change focus to set the new active pane ID instead of
relying on values.first.
- Around line 5967-5972: When updating existing panes in the loop
(tmuxPaneSurfaces[paneInfo.pane_id]), also detect when the view frame actually
changed (compare state.view.frame to tmuxPaneFrame(paneInfo)), set the new
frame, and then resize the pane's backing surface so its pixel buffer matches
the new view size (e.g. call the surface/resizing API on the state object after
changing state.view.frame). Ensure you reference tmuxPaneSurfaces,
paneInfo.pane_id, state.view.frame and tmuxPaneFrame(paneInfo) so the backing
surface is updated whenever the layout changes.
- Around line 5802-5809: The container and tmux pane host views
(tmuxPaneContainer and the pane-host view created later) are intercepting mouse
events; make them pointer-transparent by using a passthrough NSView
implementation (e.g., a PassthroughView subclass whose hitTest(_:) returns nil)
instead of plain NSView so events fall through to
GhosttyNSView.mouseDown/mouseMoved/etc.; update the allocation sites where you
create `let container = NSView(frame: ...)` (and the corresponding pane-host
creation at the 5844–5848 block) to instantiate the passthrough view class and
ensure autoresizingMask and addSubview usage remain the same.
- Around line 5790-5797: Currently you remove the pane state from
tmuxPaneSurfaces and only store regId in tmuxPendingUnregister, which prevents
handleTmuxPaneUnregistered from freeing the surface and writeContext; instead,
move the full state (not just state.regId) into tmuxPendingUnregister[paneId],
call ghostty_surface_tmux_unregister_pane(hostSurface, paneId, state.regId) as
before, and do not call state.view.removeFromSuperview() or
tmuxPaneSurfaces.removeValue(forKey: paneId) here; update
handleTmuxPaneUnregistered to look up and tear down the full state from
tmuxPendingUnregister (removing the view, freeing writeContext, and clearing the
pending entry) so the ack handler owns the teardown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdfd9414-57a1-47ef-b9b2-13a15ebf86ea

📥 Commits

Reviewing files that changed from the base of the PR and between a31b8a3 and 0220de2.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Comment on lines +5605 to +5620
// In tmux mode, route mouse events to the pane surface so Ghostty's
// native link detection works on the pane's real Terminal content.
#if DEBUG
dlog("tmux.mouseDown check tmuxControlMode=\(tmuxControlMode) paneCount=\(tmuxPaneSurfaces.count)")
#endif
if let paneState = activeTmuxPane {
let paneSurface = paneState.surface
let point = paneState.view.convert(event.locationInWindow, from: nil)
let y = paneState.view.bounds.height - point.y
#if DEBUG
dlog("tmux.mouseDown routed to pane regId=\(paneState.regId) point=(\(String(format: "%.0f", point.x)),\(String(format: "%.0f", y))) cmd=\(event.modifierFlags.contains(.command))")
#endif
ghostty_surface_mouse_pos(paneSurface, point.x, y, modsFromEvent(event))
_ = ghostty_surface_mouse_button(paneSurface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, modsFromEvent(event))
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The tmux file-path cmd+click fallback is never reached.

The tmux mouseDown branch returns right after forwarding the click to the pane surface, so handleTmuxCmdClick(...) below never runs for plain file paths. Even after wiring it in, the current matcher still stops at spaces, so common paths like ~/My File.md would truncate.

Also applies to: 5627-5670

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 5605 - 5620, The tmux branch
currently returns immediately after sending the mouse event to the pane surface,
which prevents the cmd+click file-path fallback (handleTmuxCmdClick) from
running and the path-matcher from capturing paths with spaces; update the
mouseDown handling for activeTmuxPane (paneState, paneSurface,
ghostty_surface_mouse_pos, ghostty_surface_mouse_button, modsFromEvent) so that
after forwarding the mouse event you only return if the pane surface handled a
command-click link; otherwise, detect a command-click
(event.modifierFlags.contains(.command)) and call handleTmuxCmdClick(...) as a
fallback before returning; also replace the current matcher logic used by
handleTmuxCmdClick with a more permissive pattern that allows spaces in file
paths (or supports quoted/tilde paths) so paths like "~/My File.md" are matched
fully.

Tim-Feng and others added 2 commits March 20, 2026 17:19
Add a green status bar at the bottom of tmux pane surfaces showing
session name, window list, pane title, and time — matching tmux's
default status-left/status-right format.

Implementation:
- NSView with green background + left/right NSTextFields
- Queries tmux display-message with #{T:status-left/right} format
- Targets specific pane ID (-t %paneId) for correct session context
- Auto-refreshes every 15 seconds via Timer
- Pane surface frame adjusted to reserve space above status bar

Known limitations (TODO):
- Query uses external tmux process (should use control channel)
- No -L/-S socket parameter (breaks non-default server)
- paneId fixed at creation time (stale on window switch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Prefix key state machine: Ctrl+B enters pending mode, next key
  resolves to tmux command (loaded dynamically from list-keys -T prefix)
- Ctrl+B D detach works: %exit parsed by control.zig, tmuxExit()
  cleans up pane surfaces, parser reset to ground state
- ghostty_surface_tmux_command() C API for raw tmux commands
- Active pane tracking + select-pane sync on click
- tmux status bar with session/window info (15s auto-refresh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:5012">
P2: Keyboard prefix pane-switch commands do not synchronize `tmuxActivePaneId`, so subsequent key/IME input can be routed to the wrong tmux pane.</violation>

<violation number="2" location="Sources/GhosttyTerminalView.swift:5960">
P2: Status bar polling is permanently bound to the initial pane ID, so after pane/window changes it can query a stale pane and show incorrect/empty status.</violation>

<violation number="3" location="Sources/GhosttyTerminalView.swift:6227">
P2: tmux status/prefix queries spawn an unbound external `tmux` client instead of using the active control-mode session context, risking wrong/empty bindings and status data.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift
#if DEBUG
dlog("tmux.prefix EXEC key=\(key) cmd=\(command)")
#endif
tmuxSendCommand(command)

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Keyboard prefix pane-switch commands do not synchronize tmuxActivePaneId, so subsequent key/IME input can be routed to the wrong tmux pane.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 5012:

<comment>Keyboard prefix pane-switch commands do not synchronize `tmuxActivePaneId`, so subsequent key/IME input can be routed to the wrong tmux pane.</comment>

<file context>
@@ -4957,6 +4988,44 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
+                        #if DEBUG
+                        dlog("tmux.prefix EXEC key=\(key) cmd=\(command)")
+                        #endif
+                        tmuxSendCommand(command)
+                    } else {
+                        #if DEBUG
</file context>
Fix with Cubic

Instead of a blank pane surface that requires pressing Enter, capture
the tmux pane's visible content on creation using `capture-pane -p -e`
and feed it to the pane surface via ghostty_surface_process_output.

Safety:
- Captures regId before async dispatch, verifies pane still alive on
  completion (prevents use-after-free if pane is destroyed during query)
- Uses raw stdout (no trimming) to preserve meaningful whitespace
- Dispatches process_output on main thread after lifetime check

Known limitation: query uses external tmux process (should use control
channel in the future, same as status bar and prefix config).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:6300">
P2: `tmuxQueryRaw` can deadlock by waiting for process exit before reading stdout, and it is now used by initial pane capture (`capture-pane -p -e`) which can produce large output.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +6300 to +6301
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: tmuxQueryRaw can deadlock by waiting for process exit before reading stdout, and it is now used by initial pane capture (capture-pane -p -e) which can produce large output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 6300:

<comment>`tmuxQueryRaw` can deadlock by waiting for process exit before reading stdout, and it is now used by initial pane capture (`capture-pane -p -e`) which can produce large output.</comment>

<file context>
@@ -6248,6 +6254,57 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
+        process.standardError = FileHandle.nullDevice
+        do {
+            try process.run()
+            process.waitUntilExit()
+            let data = pipe.fileHandleForReading.readDataToEndOfFile()
+            return String(data: data, encoding: .utf8) ?? ""
</file context>
Suggested change
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
Fix with Cubic

Tim-Feng and others added 2 commits March 21, 2026 03:02
Fix blank pane on tmux attach by syncing tmux client geometry and
using capture-pane with proper row truncation.

- Send refresh-client -C to sync tmux client size with pane surface
- tmuxDesiredClientSize() / tmuxCurrentClientSize() helpers
- tmuxInitialSync: truncate capture-pane to pane surface rows,
  keeping bottom rows (suffix) so prompt stays visible
- Cursor position adjusted for dropped top rows

Known issue: cursor may be offset by one row after initial sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
…ial sync

- Add opaque layer-backed background to tmux pane container so the
  host terminal's Metal layer (e.g. "Last login" text) doesn't bleed
  through gaps in the pane surface rendering.

- Simplify tmuxInitialSync back to basic capture-pane replay without
  retry/padding/truncation. The geometry sync via refresh-client -C
  handles size matching; complex alignment logic was causing more
  problems than it solved.

- Remove tmuxDesiredClientSize/tmuxCurrentClientSize helpers and
  didInitialSync/tmuxRequestedClientSize state (no longer needed).

Known limitation: re-attach after filling screen with Enter may show
content shifted up due to capture-pane returning more rows than pane
surface has (tmux defaults to 80x24 before refresh-client takes effect).
This requires Terminal.clone for a proper fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:5890">
P2: Tmux pane container caches background color at creation and is never refreshed on later theme/background updates, causing stale/mismatched background in tmux mode.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift Outdated
…ed padding

- Hide host terminal rendering via alphaValue=0 (not isHidden) to
  prevent "Last login" text bleed-through while keeping GhosttyNSView
  in the event chain for mouse handling (selection, cmd+click).
- Initial sync padding: use cursor position to calculate minimal top
  padding instead of blindly padding to fill surfaceRows. This prevents
  content from being pushed off-screen.
- Retry-based initial sync waits for tmux geometry to converge with
  pane surface rows before replaying capture-pane content.

Known limitation: re-attach after filling screen with Enter may show
content at wrong position (tmux geometry convergence timing issue).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:5887">
P1: Host view alpha can remain stuck at 0 because restoration is tied only to tmux cleanup, while some windows-changed paths return without cleanup.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// Hide host terminal rendering but keep the view in the event
// chain for mouse handling. alphaValue=0 makes the Metal layer
// invisible without setting isHidden (which breaks hit-testing).
self.alphaValue = 0

@cubic-dev-ai cubic-dev-ai Bot Mar 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Host view alpha can remain stuck at 0 because restoration is tied only to tmux cleanup, while some windows-changed paths return without cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 5887:

<comment>Host view alpha can remain stuck at 0 because restoration is tied only to tmux cleanup, while some windows-changed paths return without cleanup.</comment>

<file context>
@@ -5880,15 +5880,11 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
+            // Hide host terminal rendering but keep the view in the event
+            // chain for mouse handling. alphaValue=0 makes the Metal layer
+            // invisible without setting isHidden (which breaks hit-testing).
+            self.alphaValue = 0
             // Walk up: GhosttyNSView → documentView → clipView → scrollView → GhosttySurfaceScrollView
             if let parentView = enclosingScrollView?.superview {
</file context>
Fix with Cubic

@Tim-Feng

Tim-Feng commented Apr 4, 2026

Copy link
Copy Markdown
Author

hmm... this PR looks good

Hi, you may want to try "Tidey", rewrite on iTerm2, it's cmux+editor https://github.com/Tim-Feng/Tidey

This branch has not been deployed

No deployments
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.

3 participants