Skip to content

Fix tmux compat store decoding, layout cleanup, and cross-workspace fallback - #2207

Merged
lawrencecchen merged 5 commits into
mainfrom
fix-tmux-compat-store-hardening
Mar 27, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
fix-tmux-compat-store-hardening

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Backward-compatible store decoding: Added custom init(from:) to TmuxCompatStore using decodeIfPresent for all fields. Older store files missing mainVerticalLayouts or lastSplitSurface keys now decode gracefully instead of throwing and resetting the entire store (wiping buffers and hooks).
  • Clear main-vertical state on layout change: When select-layout is called with a non-main-vertical layout (e.g. tiled, even-horizontal), the stale mainVerticalLayouts entry for that workspace is now removed. Previously it persisted and incorrectly redirected future splits.
  • Cross-workspace surface fallback: In split-window, the caller-anchoring block now only activates when resolveWorkspaceId succeeds. The previous ?? target.workspaceId fallback could pair the caller's surface with a different workspace, creating an invalid cross-workspace split.

Test plan

  • Verify existing tmux compat store files without mainVerticalLayouts/lastSplitSurface keys load without losing buffers/hooks
  • Verify select-layout tiled clears main-vertical state and subsequent splits use default behavior
  • Verify split-window with an unresolvable caller workspace falls through to the normal target instead of cross-workspace split

Summary by cubic

Hardens the tmux compatibility layer to safely load older stores, clean up stale main‑vertical state, avoid cross‑workspace splits, resolve select-layout pane targets, and error on bad -t targets. Also auto‑equalizes teammate panes after splits and DRYs shared launcher code (no behavior changes).

  • Changes
    • Store decoding: custom init(from:) with decodeIfPresent so missing mainVerticalLayouts/lastSplitSurface don’t reset the store; preserves buffers/hooks.
    • Layout and anchoring: clear mainVerticalLayouts and lastSplitSurface when non‑main‑vertical layouts are selected; resolve select-layout -t via pane first and error if an explicit -t can’t be resolved (only default to the current workspace when no -t is given); in split-window, only anchor to the caller when resolveWorkspaceId succeeds to avoid cross‑workspace splits and defer store load unless anchoring is used.
    • Auto equalize: after teammate split-window, call workspace.equalize_splits with orientation: "vertical" to evenly distribute the agent column while preserving the leader divider.
    • Refactor: extracted shared helpers (configureTmuxCompatEnvironment, createTmuxCompatShimDirectory, resolveExecutableInSearchPath) and renamed types to TmuxCompat*; Claude Teams and OMO launchers now use the shared code.

Written for commit 2a5e6e1. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented unwanted cross-workspace pairing when creating teammate splits.
    • Made configuration-store decoding backward-compatible to avoid wiping existing settings.
  • Improvements

    • Automatically equalizes newly created split windows (best-effort).
    • Clears main-vertical layout state when selecting non-standard layouts.
    • More reliable resolution of integration executables and environment setup for related shims.
  • Refactor

    • Consolidated shim creation and environment configuration into shared flows.

@vercel

vercel Bot commented Mar 26, 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 Mar 27, 2026 1:47am

@coderabbitai

coderabbitai Bot commented Mar 26, 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

Renames tmux-compat focused context, consolidates executable resolution, environment and shim setup into shared helpers, adjusts teammate split anchoring and post-split equalization, clears main-vertical state on non-main-vertical selects, and makes TmuxCompatStore decoding backward-compatible (adds custom decoder and explicit init).

Changes

Cohort / File(s) Summary
Tmux-compat CLI and store
CLI/cmux.swift
Renamed ClaudeTeamsFocusedContext → TmuxCompatFocusedContext and updated related helpers; added generic resolveExecutableInSearchPath(...) used by resolveClaudeExecutable; consolidated environment/config and shim directory creation into shared configureTmuxCompatEnvironment(...) and createTmuxCompatShimDirectory(...); changed teammate split anchoring to require successful caller workspace resolution and added best-effort workspace.equalize_splits(..., orientation: "vertical"); on select-layout clear mainVerticalLayouts[workspaceId] when non-main-vertical is chosen; added init(from:) and explicit init() to TmuxCompatStore for backward-compatible decoding.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/cmux.swift
    participant Workspace as Workspace service
    participant Store as TmuxCompatStore
    participant FS as Filesystem / Shim scripts

    CLI->>FS: createTmuxCompatShimDirectory(directoryName, tmuxShimScript)
    CLI->>CLI: configureTmuxCompatEnvironment(..., tmuxPathPrefix, cmuxBinEnvVar, ...)
    CLI->>Workspace: resolveWorkspaceId(callerWorkspace, client)
    alt resolution succeeds
        CLI->>Store: read caller anchoring info
        CLI->>Workspace: create teammate split(s) in target.workspaceId
        CLI->>Workspace: equalize_splits(target.workspaceId, orientation: "vertical") (best-effort)
        CLI->>Store: update mainVerticalLayouts / lastSplitSurface
    else resolution fails
        CLI->>Workspace: create teammate split(s) without caller anchoring
    end
    CLI->>Workspace: select-layout(layoutName, -t target)
    alt non-empty and not main-vertical & workspace resolved
        CLI->>Store: clear mainVerticalLayouts[workspaceId]
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through sockets, shims, and names,
I nudged the panes and balanced frames,
Renamed my hat and cleaned the nest,
Equalized splits — at my behest,
A tiny rabbit claps in fuzzy claims! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main changes: store decoding backward-compatibility, layout cleanup, and cross-workspace fallback prevention.
Description check ✅ Passed PR description is comprehensive with clear summary of changes, detailed test plan, and demonstrates deep understanding of the issues being fixed.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-tmux-compat-store-hardening

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.

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

No issues found across 1 file

@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three related bugs in the tmux compatibility layer: older store files without mainVerticalLayouts/lastSplitSurface keys now decode gracefully (preserving buffers/hooks) via a custom init(from:) using decodeIfPresent; switching to a non-main-vertical layout (e.g. tiled) now removes the stale mainVerticalLayouts entry so future splits fall through to default behavior; and the cross-workspace fallback in split-window (?? target.workspaceId) has been removed so caller anchoring only activates when the caller's workspace resolves successfully.\n\n- The custom init(from:) + init() {} pair is correctly structured — synthesized encode(to:) and CodingKeys are unaffected.\n- The else if !layoutName.isEmpty guard correctly skips the cleanup when select-layout is called with no argument (re-apply current layout).\n- Minor: the same non-main-vertical cleanup block removes mainVerticalLayouts[workspaceId] but leaves lastSplitSurface[workspaceId] intact, which could cause a stale seed if select-layout main-vertical is called again after a tiled transition (see inline comment).\n- Pre-existing: loadTmuxCompatStore() is called unconditionally before the optional-binding guard in split-window, wasting a file-system read on every non-agent split.

Confidence Score: 4/5

Safe to merge; all three fixes are logically correct and address real bugs with no regressions introduced.

The backward-compat decoder, cross-workspace guard removal, and layout-change cleanup are all well-targeted and correct. The only open item is a minor P2 suggestion to also clear lastSplitSurface alongside mainVerticalLayouts on non-main-vertical layout selection, which is an edge-case seeding issue rather than a production reliability risk.

CLI/cmux.swift — specifically the else-if !layoutName.isEmpty block and the unconditional store load before the split-window if-let guard.

Important Files Changed

Filename Overview
CLI/cmux.swift Three targeted fixes: backward-compatible TmuxCompatStore decoding via custom init(from:), clearing mainVerticalLayouts on non-main-vertical select-layout, and removing the cross-workspace fallback in split-window caller anchoring. All three changes are logically sound; minor P2 note about lastSplitSurface not being cleared alongside mainVerticalLayouts.

Sequence Diagram

sequenceDiagram
    participant Agent as Claude Agent
    participant CLI as cmux CLI
    participant Store as TmuxCompatStore (disk)
    participant App as cmux App

    Note over CLI,Store: split-window (fixed cross-workspace fallback)
    Agent->>CLI: split-window -t <pane>
    CLI->>CLI: tmuxCallerSurfaceHandle() + tmuxCallerWorkspaceHandle()
    CLI->>App: resolveWorkspaceId(callerWorkspace)
    alt resolveWorkspaceId succeeds
        App-->>CLI: wsId
        CLI->>Store: loadTmuxCompatStore()
        Store-->>CLI: store
        alt mainVerticalLayouts[wsId] exists
            CLI->>App: surface.split (right column, direction=down)
        else first split
            CLI->>App: surface.split (direction=right, anchor=callerSurface)
        end
    else resolveWorkspaceId fails (cross-workspace guard)
        CLI->>App: surface.split (use original target, no anchoring)
    end
    App-->>CLI: surfaceId
    CLI->>Store: saveTmuxCompatStore (update lastSplitSurface / mainVerticalLayouts)

    Note over CLI,Store: select-layout (fixed stale state cleanup)
    Agent->>CLI: select-layout tiled -t <workspace>
    CLI->>App: tmuxResolveWorkspaceTarget
    App-->>CLI: workspaceId
    CLI->>Store: loadTmuxCompatStore()
    Store-->>CLI: store
    alt mainVerticalLayouts[workspaceId] existed
        CLI->>Store: removeValue + saveTmuxCompatStore
    end

    Note over CLI,Store: load (fixed backward-compat decode)
    CLI->>Store: JSONDecoder.decode(TmuxCompatStore)
    alt older file missing mainVerticalLayouts/lastSplitSurface
        Store-->>CLI: decodeIfPresent → default [:]
    else current file
        Store-->>CLI: full store
    end
Loading

Reviews (1): Last reviewed commit: "Fix tmux compat store decoding, layout c..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated
// successfully. Falling back to target.workspaceId would pair
// the caller's surface with a different workspace, creating an
// invalid cross-workspace split.
let store = loadTmuxCompatStore()

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.

P2 Unconditional store load before optional-binding guard

loadTmuxCompatStore() (a file-system read) is executed unconditionally on every split-window call, even when tmuxCallerSurfaceHandle(), tmuxCallerWorkspaceHandle(), or resolveWorkspaceId fail and the store is never consulted. The store load is only needed when the if let block below actually executes.

Consider moving the let store = loadTmuxCompatStore() line inside the if let body (before the mainVerticalLayouts lookup) to avoid the unnecessary I/O on every non-agent-team split:

Suggested change
let store = loadTmuxCompatStore()
if let callerSurface = tmuxCallerSurfaceHandle(),
let callerWorkspace = tmuxCallerWorkspaceHandle(),
let wsId = try? resolveWorkspaceId(callerWorkspace, client: client) {
let store = loadTmuxCompatStore()

(This is a pre-existing pattern, not a regression from this PR, but the fix here is a good moment to address it.)

Comment thread CLI/cmux.swift
Comment on lines +9861 to 9869
} else if !layoutName.isEmpty {
// Non-main-vertical layout selected: clear stale state so
// future splits don't incorrectly redirect to the old column.
let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
var store = loadTmuxCompatStore()
if store.mainVerticalLayouts.removeValue(forKey: workspaceId) != nil {
try saveTmuxCompatStore(store)
}
}

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.

P2 lastSplitSurface not cleared alongside mainVerticalLayouts

The comment says "clear stale state," but only mainVerticalLayouts[workspaceId] is removed. lastSplitSurface[workspaceId] is left with the surface ID from before the layout switch.

This matters on the path: split-window → select-layout main-vertical → select-layout tiled → select-layout main-vertical again. On the second main-vertical call, mainVerticalLayouts[workspaceId] is nil (correctly cleared here), so the code at line 9854 falls back to store.lastSplitSurface[workspaceId] to seed lastColumnSurfaceId. That stale value refers to a pane whose position may be arbitrary after the tiled re-layout.

Consider also removing lastSplitSurface[workspaceId] in the same block, and only writing the store when either removal found an entry, to keep both fields in sync.

@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 `@CLI/cmux.swift`:
- Around line 9861-9868: The code currently calls
tmuxResolveWorkspaceTarget(...) with parsed.value("-t") for select-layout -t
targets, but select-layout accepts pane targets; replace the resolution with
tmuxResolvePaneTarget(parsed.value("-t"), client: client).workspaceId to get the
correct workspaceId when a -t is provided, and fall back to
tmuxResolveWorkspaceTarget(nil, client: client) (or the existing
tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) path) when
parsed.value("-t") is empty or absent; then continue to loadTmuxCompatStore(),
removeValue(forKey: workspaceId) from store.mainVerticalLayouts, and call
saveTmuxCompatStore(store) as before.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8bac695-b5e7-489d-8c82-79b856f3e8b8

📥 Commits

Reviewing files that changed from the base of the PR and between f507fd8 and fd7d56e.

📒 Files selected for processing (1)
  • CLI/cmux.swift

Comment thread CLI/cmux.swift
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@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)
CLI/cmux.swift (1)

9866-9873: ⚠️ Potential issue | 🟠 Major

select-layout -t still uses a workspace-only resolver

At Line 9869, tmuxResolveWorkspaceTarget(parsed.value("-t"), ...) can reject pane-style -t targets. For tmux-compat behavior, resolve -t via tmuxResolvePaneTarget(...).workspaceId when provided, and only fall back to workspace resolution when -t is absent.

🔧 Proposed fix
-                let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
+                let workspaceId = try {
+                    if let target = parsed.value("-t") {
+                        return try tmuxResolvePaneTarget(target, client: client).workspaceId
+                    }
+                    return try tmuxResolveWorkspaceTarget(nil, client: client)
+                }()
-                let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
+                let workspaceId = try {
+                    if let target = parsed.value("-t") {
+                        return try tmuxResolvePaneTarget(target, client: client).workspaceId
+                    }
+                    return try tmuxResolveWorkspaceTarget(nil, client: client)
+                }()
#!/bin/bash
# Verify current select-layout -t callsites and relevant resolver behavior.
rg -n -C3 'case "select-layout"|tmuxResolveWorkspaceTarget\(parsed.value\("-t"\)|func tmuxResolveWorkspaceTarget|func tmuxResolvePaneTarget|func tmuxPaneSelector' CLI/cmux.swift
In tmux, what targets does `select-layout -t` accept? Are pane targets like `%1` and `session:window.pane` valid?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 9866 - 9873, The select-layout branch currently
always calls tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
which rejects pane-style -t targets; change it to first check if
parsed.value("-t") is non-empty and try resolving a pane target via
tmuxResolvePaneTarget(parsed.value("-t"), client: client) and use its
.workspaceId when that returns a value, otherwise fall back to the existing
tmuxResolveWorkspaceTarget(...) behavior; update the block around
parsed.value("-t"), tmuxResolvePaneTarget, and tmuxResolveWorkspaceTarget to
perform the try/optional unwrapping and only call saveTmuxCompatStore when
mainVerticalLayouts actually changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 9866-9873: The select-layout branch currently always calls
tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) which rejects
pane-style -t targets; change it to first check if parsed.value("-t") is
non-empty and try resolving a pane target via
tmuxResolvePaneTarget(parsed.value("-t"), client: client) and use its
.workspaceId when that returns a value, otherwise fall back to the existing
tmuxResolveWorkspaceTarget(...) behavior; update the block around
parsed.value("-t"), tmuxResolvePaneTarget, and tmuxResolveWorkspaceTarget to
perform the try/optional unwrapping and only call saveTmuxCompatStore when
mainVerticalLayouts actually changed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a638de8e-b61c-4d34-9723-d750769aa961

📥 Commits

Reviewing files that changed from the base of the PR and between fd7d56e and 3898de4.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • Sources/TerminalController.swift

…allback

Three hardening fixes for the tmux compatibility layer:

1. Add custom init(from:) to TmuxCompatStore using decodeIfPresent so
   older store files missing mainVerticalLayouts/lastSplitSurface keys
   decode gracefully instead of silently resetting to an empty store.

2. Clear mainVerticalLayouts entry when a non-main-vertical layout is
   selected, preventing stale state from redirecting future splits.

3. Only enter caller-anchoring block in split-window when
   resolveWorkspaceId succeeds, avoiding cross-workspace splits when
   the fallback target.workspaceId differs from the caller's workspace.
@lawrencecchen
lawrencecchen force-pushed the fix-tmux-compat-store-hardening branch from 3898de4 to f1aa0b3 Compare March 26, 2026 23:16
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

After each teammate split-window, call workspace.equalize_splits with
orientation: "vertical" to evenly distribute panes in the agent column
without affecting the leader/column horizontal divider.

Uses the equalize_splits socket method from the omo integration
(PR #2087). The equalization works synchronously because bonsplit's
setDividerPosition sets ratios on the internal split state directly,
no layout flush needed.
@lawrencecchen
lawrencecchen force-pushed the fix-tmux-compat-store-hardening branch from ca3e489 to e73ee78 Compare March 27, 2026 00:04
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@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 (1)
CLI/cmux.swift (1)

10384-10391: ⚠️ Potential issue | 🟠 Major

select-layout -t target resolution issue persists here (duplicate).

Line 10387 adds another tmuxResolveWorkspaceTarget(parsed.value("-t"), ...) call, which repeats the previously flagged select-layout pane-target handling problem and needlessly re-resolves the same workspace.

💡 Proposed fix
-                let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
                 var store = loadTmuxCompatStore()
                 if store.mainVerticalLayouts.removeValue(forKey: workspaceId) != nil {
                     try saveTmuxCompatStore(store)
                 }

And ensure the earlier workspaceId for select-layout is resolved via pane-aware logic (tmuxResolvePaneTarget(...).workspaceId when -t is provided).

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

In `@CLI/cmux.swift` around lines 10384 - 10391, The code duplicates target
resolution by calling tmuxResolveWorkspaceTarget(parsed.value("-t"), client:
client) again; instead reuse the earlier resolved workspace id and, when a -t
was supplied, resolve it via pane-aware logic: call
tmuxResolvePaneTarget(parsed.value("-t"), client: client).workspaceId (or
equivalent pane-aware resolution) instead of tmuxResolveWorkspaceTarget, and
then use that workspaceId when mutating store.mainVerticalLayouts and calling
saveTmuxCompatStore; ensure you reference parsed.value("-t"),
tmuxResolvePaneTarget, tmuxResolveWorkspaceTarget, store.mainVerticalLayouts,
and saveTmuxCompatStore to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 10086-10092: The call to client.sendV2 for
"workspace.equalize_splits" should pass deferred: true so equalization runs
after the next layout pass; update the params dictionary provided to sendV2 in
the block that calls workspace.equalize_splits (where client.sendV2 is invoked
with "workspace_id": target.workspaceId and "orientation": "vertical") to
include "deferred": true so the equalization uses the updated layout tree.

---

Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 10384-10391: The code duplicates target resolution by calling
tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) again; instead
reuse the earlier resolved workspace id and, when a -t was supplied, resolve it
via pane-aware logic: call tmuxResolvePaneTarget(parsed.value("-t"), client:
client).workspaceId (or equivalent pane-aware resolution) instead of
tmuxResolveWorkspaceTarget, and then use that workspaceId when mutating
store.mainVerticalLayouts and calling saveTmuxCompatStore; ensure you reference
parsed.value("-t"), tmuxResolvePaneTarget, tmuxResolveWorkspaceTarget,
store.mainVerticalLayouts, and saveTmuxCompatStore to locate the change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25bc3958-e94c-4e28-bbe7-debe9bcd0c74

📥 Commits

Reviewing files that changed from the base of the PR and between f1aa0b3 and e73ee78.

📒 Files selected for processing (1)
  • CLI/cmux.swift

Comment thread CLI/cmux.swift
Comment on lines +10086 to +10092
// Equalize vertical splits so teammate panes are evenly distributed.
// Use orientation: "vertical" to only equalize the agent column,
// preserving the leader/column horizontal divider position.
_ = try? client.sendV2(method: "workspace.equalize_splits", params: [
"workspace_id": target.workspaceId,
"orientation": "vertical"
])

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

Add deferred equalization to avoid post-split layout races.

Line 10089 should include deferred: true so equalization runs after the next layout pass; otherwise the new split can be equalized against a stale tree.

💡 Proposed fix
             _ = try? client.sendV2(method: "workspace.equalize_splits", params: [
                 "workspace_id": target.workspaceId,
-                "orientation": "vertical"
+                "orientation": "vertical",
+                "deferred": true
             ])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 10086 - 10092, The call to client.sendV2 for
"workspace.equalize_splits" should pass deferred: true so equalization runs
after the next layout pass; update the params dictionary provided to sendV2 in
the block that calls workspace.equalize_splits (where client.sendV2 is invoked
with "workspace_id": target.workspaceId and "orientation": "vertical") to
include "deferred": true so the equalization uses the updated layout tree.

Extract shared functions from the nearly-identical claude-teams and omo
integration code:

- configureTmuxCompatEnvironment: parameterized env setup (tmux path
  prefix, bin env var, term override, extra vars)
- createTmuxCompatShimDirectory: shared tmux shim creation with
  writeShimIfChanged
- resolveExecutableInSearchPath: generic PATH search with optional
  skip predicate
- Rename ClaudeTeamsFocusedContext -> TmuxCompatFocusedContext,
  claudeTeamsFocusedContext -> tmuxCompatFocusedContext,
  claudeTeamsResolvedSocketPath -> tmuxCompatResolvedSocketPath

Both integrations are now thin wrappers over the shared functions.
No behavior changes.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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

Caution

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

⚠️ Outside diff range comments (1)
CLI/cmux.swift (1)

9861-9885: ⚠️ Potential issue | 🟡 Minor

Pass the actual OpenCode port to environment configuration.

The port mismatch is real: if commandArgs contains --port 8080, opencode runs on that port, but OPENCODE_PORT defaults to 4096 in the environment. While OPENCODE_PORT is primarily a fallback in plugin logic (not the main discovery mechanism), inconsistency here could cause issues when the primary server URL is unavailable.

Suggested fix

Extract the effective port from commandArgs before calling configureOMOEnvironment and pass it through:

-    private func runOMO(
-        commandArgs: [String],
-        socketPath: String,
-        explicitPassword: String?
-    ) throws {
+    private func runOMO(
+        commandArgs: [String],
+        socketPath: String,
+        explicitPassword: String?
+    ) throws {
         // Ensure oh-my-opencode plugin is registered and installed
         try omoEnsurePlugin()
 
         let processEnvironment = ProcessInfo.processInfo.environment
         var launcherEnvironment = processEnvironment
         launcherEnvironment["CMUX_SOCKET_PATH"] = socketPath
         launcherEnvironment["CMUX_SOCKET"] = socketPath
         if let explicitPassword,
            !explicitPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
             launcherEnvironment["CMUX_SOCKET_PASSWORD"] = explicitPassword
         }
+        // Determine effective port before configuring environment
+        var effectivePort = 4096
+        if let portIdx = commandArgs.firstIndex(of: "--port"),
+           portIdx + 1 < commandArgs.count,
+           let port = Int(commandArgs[commandArgs.index(after: portIdx)]) {
+            effectivePort = port
+        }
         let shimDirectory = try createOMOShimDirectory()
         let executablePath = resolvedExecutableURL()?.path ?? (args.first ?? "cmux")
         let focusedContext = tmuxCompatFocusedContext(
             processEnvironment: launcherEnvironment,
             explicitPassword: explicitPassword
         )
         let openCodeExecutablePath = resolveOpenCodeExecutable(searchPath: launcherEnvironment["PATH"])
         configureOMOEnvironment(
             processEnvironment: launcherEnvironment,
             shimDirectory: shimDirectory,
             executablePath: executablePath,
             socketPath: socketPath,
             explicitPassword: explicitPassword,
-            focusedContext: focusedContext
+            focusedContext: focusedContext,
+            openCodePort: effectivePort
         )

Then update configureOMOEnvironment to use the passed port:

 private func configureOMOEnvironment(
     processEnvironment: [String: String],
     shimDirectory: URL,
     executablePath: String,
     socketPath: String,
     explicitPassword: String?,
-    focusedContext: TmuxCompatFocusedContext?
+    focusedContext: TmuxCompatFocusedContext?,
+    openCodePort: Int
 ) {
-    var extraEnvVars: [(key: String, value: String)] = []
-    if processEnvironment["OPENCODE_PORT"] == nil {
-        extraEnvVars.append((key: "OPENCODE_PORT", value: "4096"))
-    }
+    let extraEnvVars: [(key: String, value: String)] = [
+        (key: "OPENCODE_PORT", value: String(openCodePort))
+    ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 9861 - 9885, The OPENCODE_PORT fallback is
hardcoded to 4096; extract the effective port from commandArgs where the server
is started (e.g. parse "--port" or equivalent from commandArgs) and pass that
value into configureOMOEnvironment (add a new parameter like opencodePort).
Inside configureOMOEnvironment, use the passed opencodePort when building
extraEnvVars (set OPENCODE_PORT to that value only if not already present in
processEnvironment) and then call configureTmuxCompatEnvironment as before so
the env reflects the actual server port rather than the fixed 4096.
♻️ Duplicate comments (1)
CLI/cmux.swift (1)

10369-10403: ⚠️ Potential issue | 🟠 Major

Resolve select-layout -t through a pane target.

tmux accepts pane targets for select-layout -t; routing that flag through tmuxResolveWorkspaceTarget(...) still rejects valid inputs like %1 and session:window.pane. Resolve the pane first, reuse its workspaceId, and keep the nil fallback when -t is absent.

💡 Proposed fix
         case "select-layout":
             let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: [])
             let layoutName = parsed.positional.first ?? ""
-            let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
+            let workspaceId = try {
+                if let target = parsed.value("-t") {
+                    return try tmuxResolvePaneTarget(target, client: client).workspaceId
+                }
+                return try tmuxResolveWorkspaceTarget(nil, client: client)
+            }()
             if layoutName == "main-vertical" || layoutName == "main-horizontal" {
                 // For main-* layouts, only equalize the agent column (vertical splits),
                 // not the top-level horizontal split between main and agents.
                 let orientation = layoutName == "main-vertical" ? "vertical" : "horizontal"
                 _ = try? client.sendV2(method: "workspace.equalize_splits", params: [
@@
             } else if !layoutName.isEmpty {
                 // Non-main-vertical layout selected: clear stale state so
                 // future splits don't incorrectly redirect to the old column.
-                let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
                 var store = loadTmuxCompatStore()
                 if store.mainVerticalLayouts.removeValue(forKey: workspaceId) != nil {
                     try saveTmuxCompatStore(store)
                 }
             }
In tmux, does `select-layout -t` accept pane targets such as `%1` or `session:window.pane`, and does it apply the layout to the containing window?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 10369 - 10403, The select-layout handling
currently passes parsed.value("-t") directly to tmuxResolveWorkspaceTarget which
rejects pane-style targets like "%1" or "session:window.pane"; change the logic
in the select-layout case (around parseTmuxArguments,
tmuxResolveWorkspaceTarget) to first check parsed.value("-t") and if it looks
like a pane target (e.g. starts with "%" or contains ":"/".") call
tmuxResolvePaneTarget (or the existing pane-resolving function) to obtain the
pane and then derive/reuse its workspaceId for subsequent calls; keep the
existing nil fallback when -t is absent so behavior is unchanged for no target.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 9861-9885: The OPENCODE_PORT fallback is hardcoded to 4096;
extract the effective port from commandArgs where the server is started (e.g.
parse "--port" or equivalent from commandArgs) and pass that value into
configureOMOEnvironment (add a new parameter like opencodePort). Inside
configureOMOEnvironment, use the passed opencodePort when building extraEnvVars
(set OPENCODE_PORT to that value only if not already present in
processEnvironment) and then call configureTmuxCompatEnvironment as before so
the env reflects the actual server port rather than the fixed 4096.

---

Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 10369-10403: The select-layout handling currently passes
parsed.value("-t") directly to tmuxResolveWorkspaceTarget which rejects
pane-style targets like "%1" or "session:window.pane"; change the logic in the
select-layout case (around parseTmuxArguments, tmuxResolveWorkspaceTarget) to
first check parsed.value("-t") and if it looks like a pane target (e.g. starts
with "%" or contains ":"/".") call tmuxResolvePaneTarget (or the existing
pane-resolving function) to obtain the pane and then derive/reuse its
workspaceId for subsequent calls; keep the existing nil fallback when -t is
absent so behavior is unchanged for no target.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5670d7ce-e754-4869-b6ea-f3fde30f79b2

📥 Commits

Reviewing files that changed from the base of the PR and between e73ee78 and 2034079.

📒 Files selected for processing (1)
  • CLI/cmux.swift

1. Move loadTmuxCompatStore() inside the caller-anchoring if-let so
   it's only called when env vars are present (avoids unnecessary
   file I/O on non-agent splits).

2. Clear lastSplitSurface alongside mainVerticalLayouts when switching
   away from main-vertical layout, preventing stale seed values on
   re-entry.

3. Resolve select-layout -t via tmuxResolvePaneTarget first (tmux
   accepts pane targets like %1), falling back to workspace target.
   Removes duplicate workspace resolution in the else branch.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@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="CLI/cmux.swift">

<violation number="1" location="CLI/cmux.swift:10379">
P2: Do not fall back to the current workspace when an explicit `-t` target fails to resolve; this can apply layout changes to the wrong workspace.</violation>
</file>

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

Comment thread CLI/cmux.swift Outdated
When an explicit -t target fails to resolve, error instead of silently
falling back to the caller's current workspace. Only use the current
workspace as default when no -t was provided.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@lawrencecchen
lawrencecchen merged commit 1826cb5 into main Mar 27, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the fix-tmux-compat-store-hardening branch March 27, 2026 01:54
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…allback (manaflow-ai#2207)

* Fix tmux compat store decoding, layout cleanup, and cross-workspace fallback

Three hardening fixes for the tmux compatibility layer:

1. Add custom init(from:) to TmuxCompatStore using decodeIfPresent so
   older store files missing mainVerticalLayouts/lastSplitSurface keys
   decode gracefully instead of silently resetting to an empty store.

2. Clear mainVerticalLayouts entry when a non-main-vertical layout is
   selected, preventing stale state from redirecting future splits.

3. Only enter caller-anchoring block in split-window when
   resolveWorkspaceId succeeds, avoiding cross-workspace splits when
   the fallback target.workspaceId differs from the caller's workspace.

* Add auto-equalize after teammate splits

After each teammate split-window, call workspace.equalize_splits with
orientation: "vertical" to evenly distribute panes in the agent column
without affecting the leader/column horizontal divider.

Uses the equalize_splits socket method from the omo integration
(PR manaflow-ai#2087). The equalization works synchronously because bonsplit's
setDividerPosition sets ratios on the internal split state directly,
no layout flush needed.

* Refactor: DRY up claude-teams and omo shared launcher code

Extract shared functions from the nearly-identical claude-teams and omo
integration code:

- configureTmuxCompatEnvironment: parameterized env setup (tmux path
  prefix, bin env var, term override, extra vars)
- createTmuxCompatShimDirectory: shared tmux shim creation with
  writeShimIfChanged
- resolveExecutableInSearchPath: generic PATH search with optional
  skip predicate
- Rename ClaudeTeamsFocusedContext -> TmuxCompatFocusedContext,
  claudeTeamsFocusedContext -> tmuxCompatFocusedContext,
  claudeTeamsResolvedSocketPath -> tmuxCompatResolvedSocketPath

Both integrations are now thin wrappers over the shared functions.
No behavior changes.

* Address PR review comments: store load, stale state, pane targets

1. Move loadTmuxCompatStore() inside the caller-anchoring if-let so
   it's only called when env vars are present (avoids unnecessary
   file I/O on non-agent splits).

2. Clear lastSplitSurface alongside mainVerticalLayouts when switching
   away from main-vertical layout, preventing stale seed values on
   re-entry.

3. Resolve select-layout -t via tmuxResolvePaneTarget first (tmux
   accepts pane targets like %1), falling back to workspace target.
   Removes duplicate workspace resolution in the else branch.

* Fix select-layout -t fallback: don't apply to wrong workspace

When an explicit -t target fails to resolve, error instead of silently
falling back to the caller's current workspace. Only use the current
workspace as default when no -t was provided.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 2a5e6e14 Deployed Mar 27, 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.

1 participant