Repository navigation
Fix claude-teams pane anchoring with main-vertical layout - #2119
Conversation
Claude's agent teams sends `split-window -h` for each teammate then `select-layout main-vertical` to stack them vertically. Three fixes: 1. Implement select-layout main-vertical: track layout state in the tmux compat store so subsequent horizontal splits of the leader pane get redirected to vertical splits of the right-side column. 2. Pass focus:false to surface.split from the split-window handler so internal bonsplit focus stays on the leader pane. 3. Fix tmuxCompatStoreURL to respect $HOME env var (was using NSString.expandingTildeInPath which ignores $HOME). Closes #2118
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughUpdated tmux-compat split handling to support "main-vertical" teammate layouts: splits now target a tracked right-column surface (or leader's canonical surface) with focus explicitly disabled, persistent per-workspace state is recorded, and CLI split requests accept an explicit focus flag. Changes
Sequence DiagramsequenceDiagram
participant Claude as Claude Client
participant Compat as Tmux-Compat Handler
participant Store as Compat Store
participant Surface as Surface API
Claude->>Compat: split-window -h (from leader)
Compat->>Store: maybe init/record lastSplitSurface
Compat->>Surface: surface.split(target=leaderSurface/right, focus=false)
Surface-->>Compat: new teammate surface A
Claude->>Compat: select-layout main-vertical
Compat->>Store: enable MainVerticalState for workspace
Claude->>Compat: split-window -h (leader again)
Compat->>Store: lookup right-column target (lastSplitSurface / column)
Compat->>Compat: redirect -> target=surface A, direction=down
Compat->>Surface: surface.split(target=surface A/down, focus=false)
Surface-->>Compat: new teammate surface B
Compat->>Store: update lastSplitSurface / main-vertical tracking
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d140f29181
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var mainVerticalLayouts: [String: MainVerticalState] = [:] | ||
| /// Tracks the last surface created by split-window per workspace. | ||
| /// Used to seed lastColumnSurfaceId when select-layout main-vertical | ||
| /// is called after the first split. | ||
| var lastSplitSurface: [String: String] = [:] |
There was a problem hiding this comment.
Preserve backward decoding of tmux compat store
The two new non-optional Codable properties make older tmux-compat-store.json files (written before this commit) fail to decode, and loadTmuxCompatStore() then silently falls back to an empty store. That drops previously persisted buffers/hooks on first read after upgrade, which can break tmux-compat behavior until the store is rebuilt. Please decode these new fields with backward-compatible defaults (e.g., optional fields or a custom init(from:)).
Useful? React with 👍 / 👎.
| if layoutName == "main-vertical" { | ||
| let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) |
There was a problem hiding this comment.
Reset main-vertical redirect state for other layouts
This branch only updates state when layoutName == "main-vertical" and never clears prior mainVerticalLayouts state when another layout is selected. After one main-vertical call, a later select-layout to a different layout still leaves redirection active, so subsequent split-window -h calls from the leader can be incorrectly forced into down splits using stale column state.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes the
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Claude as Claude binary
participant Shim as tmux shim (cmux)
participant Store as TmuxCompatStore (JSON)
participant Server as cmux surface.split
Note over Claude,Server: First teammate
Claude->>Shim: tmux split-window -h -t $TMUX_PANE
Shim->>Store: loadTmuxCompatStore() — no mainVertical state yet
Shim->>Server: surface.split(surface=leader, direction=right, focus=false)
Server-->>Shim: {surface_id: T1}
Shim->>Store: save lastSplitSurface[ws]=T1
Claude->>Shim: tmux select-layout main-vertical
Shim->>Store: loadTmuxCompatStore()
Note right of Shim: seedColumn = lastSplitSurface[ws] = T1
Shim->>Store: save mainVerticalLayouts[ws]={main=leader, lastCol=T1}
Note over Claude,Server: Second teammate (redirect kicks in)
Claude->>Shim: tmux split-window -h -t $TMUX_PANE
Shim->>Store: loadTmuxCompatStore() — mainVertical active, lastCol=T1
Note right of Shim: Redirect: target→T1, direction→down
Shim->>Server: surface.split(surface=T1, direction=down, focus=false)
Server-->>Shim: {surface_id: T2}
Shim->>Store: save lastSplitSurface[ws]=T2, mainVerticalLayouts[ws].lastCol=T2
Claude->>Shim: tmux select-layout main-vertical
Shim->>Store: update mainVerticalLayouts[ws].lastCol=T2 (no change)
Note over Claude,Server: Third teammate (same pattern, last col = T2)
Claude->>Shim: tmux split-window -h -t $TMUX_PANE
Shim->>Store: loadTmuxCompatStore() — lastCol=T2
Note right of Shim: Redirect: target→T2, direction→down
Shim->>Server: surface.split(surface=T2, direction=down, focus=false)
Server-->>Shim: {surface_id: T3}
Shim->>Store: save lastCol=T3
Reviews (1): Last reviewed commit: "Fix claude-teams pane anchoring: main-ve..." | Re-trigger Greptile |
| target.surfaceId == mvState.mainSurfaceId, | ||
| let lastColumn = mvState.lastColumnSurfaceId { | ||
| // Split the bottom pane of the right column downward. | ||
| target = (target.workspaceId, target.paneId, lastColumn) |
There was a problem hiding this comment.
Mismatched
paneId in redirected target
After the redirect, target.paneId still holds the leader's pane ID even though lastColumn (now target.surfaceId) belongs to a completely different teammate pane. This doesn't break anything today — paneId is unused in the subsequent surface.split call and only workspaceId / surfaceId are forwarded — but the tuple is internally inconsistent and could cause confusion if a future code path reads target.paneId expecting it to be the owning pane of target.surfaceId.
Consider either documenting the invariant explicitly or resolving the pane ID for lastColumn:
| target = (target.workspaceId, target.paneId, lastColumn) | |
| target = (target.workspaceId, nil, lastColumn) |
Using nil makes the inconsistency explicit without requiring an extra network round-trip, and matches the way tmuxResolveSurfaceTarget returns nil for paneId in the window-target path.
| proc = subprocess.run( | ||
| [cli_path, "claude-teams", "--version"], | ||
| capture_output=True, | ||
| text=True, | ||
| check=False, | ||
| env=env, | ||
| timeout=30, | ||
| ) |
There was a problem hiding this comment.
--version flag relies on fake claude ignoring its arguments
The test invokes claude-teams --version, which forwards --version to the fake claude binary. That binary is written to unconditionally run the full teammate flow regardless of the argument passed to it (it never inspects $@). So the test relies on an implicit contract: the fake binary silently ignores --version and proceeds. This is non-obvious to future maintainers and would silently stop exercising the teammate flow if a later iteration of the fake binary ever added early-exit argument handling.
Consider either:
- Dropping the
--versionand just invokingclaude-teamsdirectly (if that is a valid invocation that runs the flow), or - Using a more intent-revealing name like
cmux claude-teams run-teammate-flowif such an alias exists, or - Adding a brief inline comment explaining why
--versionis used here.
| var hooks: [String: String] = [:] | ||
| /// Tracks main-vertical layout state per workspace, keyed by workspace ID. | ||
| var mainVerticalLayouts: [String: MainVerticalState] = [:] | ||
| /// Tracks the last surface created by split-window per workspace. | ||
| /// Used to seed lastColumnSurfaceId when select-layout main-vertical | ||
| /// is called after the first split. | ||
| var lastSplitSurface: [String: String] = [:] | ||
| } |
There was a problem hiding this comment.
Store entries never purged on workspace close
mainVerticalLayouts and lastSplitSurface are keyed by workspace UUID and written on every split-window and select-layout main-vertical call, but there is no code path that removes entries when a workspace is closed (the kill-window / killw handler only calls workspace.close and returns). With UUIDs as keys the chance of a collision is negligible, but over a long-lived install the JSON store file will grow monotonically.
Consider pruning the stale entries inside the existing kill-window / killw handler after the workspace.close call, mirroring the pattern already used elsewhere in the file for buffer/hook cleanup.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
tests/test_cli_claude_teams_main_vertical.py (1)
294-320: Consider verifying RESULT_LOG contents or removing unused file creation.The fake claude script writes pane IDs to
RESULT_LOG(line 318), but the test never reads or verifies this file. While it may be useful for manual debugging, it could be:
- Removed if not needed
- Verified in assertions for additional confidence
This is a minor observation and doesn't affect test correctness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_cli_claude_teams_main_vertical.py` around lines 294 - 320, The fake "claude" stub created via make_executable writes pane IDs to RESULT_LOG but the test never consumes that file; either remove the printf >> "$RESULT_LOG" line from the generated script to avoid creating an unused artifact, or add an assertion in the test to open RESULT_LOG and validate its contents (e.g., check it contains three non-empty pane IDs or specific expected values) after invoking the stub; update references around make_executable and RESULT_LOG so the produced script and the test remain consistent.
🤖 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 9798-9815: When handling the "select-layout" case, if layoutName
is not "main-vertical" you must clear any persisted main-vertical state for that
workspace so future splits don't reuse the old column; after resolving
workspaceId (tmuxResolveWorkspaceTarget) and loading the store
(loadTmuxCompatStore()), remove store.mainVerticalLayouts[workspaceId] (or set
it to nil) and save with try saveTmuxCompatStore(store); keep the existing
main-vertical branch unchanged but ensure non-main-vertical paths explicitly
delete the entry to avoid stale redirection on later split-window -h calls.
- Around line 9535-9562: The resize handling currently returns early when tmux
emits only -x/-y (no -L/-R/-U/-D), so after you fixed split routing you also
need to apply those resize-pane deltas to the correct surface in main-vertical
mode: in runClaudeTeamsTmuxCompat(...) detect resize-pane calls that specify
width/height (-x/-y) but no direction and, if
loadTmuxCompatStore().mainVerticalLayouts contains an entry for the
target.workspaceId with target.surfaceId == mvState.mainSurfaceId and a
lastColumnSurfaceId, forward the resize to mvState.lastColumnSurfaceId (use the
same numeric -x/-y values) instead of returning early; reference the existing
target/direction handling and mvState.lastColumnSurfaceId to locate where to add
this special-case resize routing.
- Around line 9568-9575: The code currently updates
mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId for every split;
change it so lastColumnSurfaceId is only updated for splits that actually extend
the right column: detect the split kind/axis (e.g. only when the split is a
horizontal/right-column extension, e.g. splitAxis == .horizontal or splitFlag ==
"-h") and additionally validate stored state to avoid poisoning with stale IDs
by ensuring the split source matches the current expected right-column surface
(compare the pre-update
updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId or
updatedStore.lastSplitSurface[target.workspaceId] against the split source
surface id) before assigning lastColumnSurfaceId = surfaceId; leave
saveTmuxCompatStore(updatedStore) as-is but skip the lastColumnSurfaceId
mutation when these checks fail.
In `@Sources/TerminalController.swift`:
- Around line 4569-4570: The code currently passes the raw focus flag from
v2Bool into tabManager.newSplit(...), allowing non-focus-intent socket/CLI
commands (surface.split) to change app focus; instead, gate focus through
v2FocusAllowed by computing focusAllowed = v2FocusAllowed(params) and only
honoring the v2Bool(params, "focus") (or default true) when focusAllowed is
true, otherwise pass false into tabManager.newSplit/tabManager.newTerminalSplit;
update the call site (the block with v2Bool(...), tabManager.newSplit(tabId:
ws.id, ...)) to use this guarded focus value so only explicit focus-intent
commands can mutate in-app focus/selection.
---
Nitpick comments:
In `@tests/test_cli_claude_teams_main_vertical.py`:
- Around line 294-320: The fake "claude" stub created via make_executable writes
pane IDs to RESULT_LOG but the test never consumes that file; either remove the
printf >> "$RESULT_LOG" line from the generated script to avoid creating an
unused artifact, or add an assertion in the test to open RESULT_LOG and validate
its contents (e.g., check it contains three non-empty pane IDs or specific
expected values) after invoking the stub; update references around
make_executable and RESULT_LOG so the produced script and the test remain
consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 62c3caa9-93cf-46aa-9f13-420d677072e1
📒 Files selected for processing (3)
CLI/cmux.swiftSources/TerminalController.swifttests/test_cli_claude_teams_main_vertical.py
| case "select-layout": | ||
| let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: []) | ||
| let layoutName = parsed.positional.first ?? "" | ||
| if layoutName == "main-vertical" { | ||
| let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) | ||
| if let callerSurface = tmuxCallerSurfaceHandle() { | ||
| var store = loadTmuxCompatStore() | ||
| // Seed lastColumnSurfaceId from the most recent split if | ||
| // this is the first time main-vertical is set and a split | ||
| // already happened (the normal flow: split then layout). | ||
| let existingColumn = store.mainVerticalLayouts[workspaceId]?.lastColumnSurfaceId | ||
| let seedColumn = existingColumn ?? store.lastSplitSurface[workspaceId] | ||
| store.mainVerticalLayouts[workspaceId] = MainVerticalState( | ||
| mainSurfaceId: callerSurface, | ||
| lastColumnSurfaceId: seedColumn | ||
| ) | ||
| try saveTmuxCompatStore(store) | ||
| } |
There was a problem hiding this comment.
Clear persisted main-vertical state when another layout is selected.
This branch only mutates state for main-vertical. If the workspace later runs select-layout tiled/even-*, the old mainVerticalLayouts[workspaceId] entry survives and future split-window -h calls still get redirected into the remembered right column even though the layout changed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLI/cmux.swift` around lines 9798 - 9815, When handling the "select-layout"
case, if layoutName is not "main-vertical" you must clear any persisted
main-vertical state for that workspace so future splits don't reuse the old
column; after resolving workspaceId (tmuxResolveWorkspaceTarget) and loading the
store (loadTmuxCompatStore()), remove store.mainVerticalLayouts[workspaceId] (or
set it to nil) and save with try saveTmuxCompatStore(store); keep the existing
main-vertical branch unchanged but ensure non-main-vertical paths explicitly
delete the entry to avoid stale redirection on later split-window -h calls.
| let focus = v2Bool(params, "focus") ?? true | ||
| if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction, focus: focus) { |
There was a problem hiding this comment.
Gate split focus through v2FocusAllowed.
Line 4569 forwards the raw focus flag into tabManager.newSplit(...), and that flag ultimately drives Workspace.newTerminalSplit(..., focus:), which changes the focused panel. Since surface.split is not a focus-intent socket command, this still lets callers steal in-app focus by omitting focus or sending true.
Suggested fix
- let focus = v2Bool(params, "focus") ?? true
+ let focus = v2FocusAllowed(requested: v2Bool(params, "focus") ?? true)
if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction, focus: focus) {Based on learnings: Socket/CLI commands must not steal macOS app focus. Only explicit focus-intent commands may mutate in-app focus/selection.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let focus = v2Bool(params, "focus") ?? true | |
| if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction, focus: focus) { | |
| let focus = v2FocusAllowed(requested: v2Bool(params, "focus") ?? true) | |
| if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction, focus: focus) { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TerminalController.swift` around lines 4569 - 4570, The code
currently passes the raw focus flag from v2Bool into tabManager.newSplit(...),
allowing non-focus-intent socket/CLI commands (surface.split) to change app
focus; instead, gate focus through v2FocusAllowed by computing focusAllowed =
v2FocusAllowed(params) and only honoring the v2Bool(params, "focus") (or default
true) when focusAllowed is true, otherwise pass false into
tabManager.newSplit/tabManager.newTerminalSplit; update the call site (the block
with v2Bool(...), tabManager.newSplit(tabId: ws.id, ...)) to use this guarded
focus value so only explicit focus-intent commands can mutate in-app
focus/selection.
There was a problem hiding this comment.
4 issues found across 3 files
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/TerminalController.swift">
<violation number="1" location="Sources/TerminalController.swift:4569">
P1: `surface.split` bypasses focus-mutation policy by using raw `focus` instead of `v2FocusAllowed(...)`, which can unexpectedly move focus during non-focus socket commands.</violation>
</file>
<file name="CLI/cmux.swift">
<violation number="1" location="CLI/cmux.swift:9572">
P2: `lastColumnSurfaceId` is updated for every split while main-vertical is enabled, which can corrupt the right-column anchor and misroute later teammate splits.</violation>
<violation number="2" location="CLI/cmux.swift:9801">
P2: Clear stored `mainVerticalLayouts` state when `select-layout` switches to a non-`main-vertical` layout. Leaving stale state active can incorrectly force later `split-window -h` calls into `down` splits.</violation>
<violation number="3" location="CLI/cmux.swift:9838">
P1: Add backward-compatible decoding defaults for the new tmux-compat store fields. Older store files that do not contain these keys should still decode instead of resetting persisted state.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| v2MaybeSelectWorkspace(tabManager, workspace: ws) | ||
|
|
||
| if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction) { | ||
| let focus = v2Bool(params, "focus") ?? true |
There was a problem hiding this comment.
P1: surface.split bypasses focus-mutation policy by using raw focus instead of v2FocusAllowed(...), which can unexpectedly move focus during non-focus socket commands.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 4569:
<comment>`surface.split` bypasses focus-mutation policy by using raw `focus` instead of `v2FocusAllowed(...)`, which can unexpectedly move focus during non-focus socket commands.</comment>
<file context>
@@ -4566,7 +4566,8 @@ class TerminalController {
v2MaybeSelectWorkspace(tabManager, workspace: ws)
- if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction) {
+ let focus = v2Bool(params, "focus") ?? true
+ if let newId = tabManager.newSplit(tabId: ws.id, surfaceId: targetSurfaceId, direction: direction, focus: focus) {
let paneUUID = ws.paneId(forPanelId: newId)?.id
</file context>
| let focus = v2Bool(params, "focus") ?? true | |
| let focus = v2FocusAllowed(requested: v2Bool(params, "focus") ?? true) |
| var buffers: [String: String] = [:] | ||
| var hooks: [String: String] = [:] | ||
| /// Tracks main-vertical layout state per workspace, keyed by workspace ID. | ||
| var mainVerticalLayouts: [String: MainVerticalState] = [:] |
There was a problem hiding this comment.
P1: Add backward-compatible decoding defaults for the new tmux-compat store fields. Older store files that do not contain these keys should still decode instead of resetting persisted state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 9838:
<comment>Add backward-compatible decoding defaults for the new tmux-compat store fields. Older store files that do not contain these keys should still decode instead of resetting persisted state.</comment>
<file context>
@@ -9769,22 +9795,59 @@ struct CMUXCLI {
var buffers: [String: String] = [:]
var hooks: [String: String] = [:]
+ /// Tracks main-vertical layout state per workspace, keyed by workspace ID.
+ var mainVerticalLayouts: [String: MainVerticalState] = [:]
+ /// Tracks the last surface created by split-window per workspace.
+ /// Used to seed lastColumnSurfaceId when select-layout main-vertical
</file context>
…ms-pane-anchoring
…t split The split-window routing was broken in two ways: 1. After the first teammate split (right), the main-vertical state wasn't being created, so all subsequent splits also went right instead of stacking down in the right column. 2. The old code only redirected splits when main-vertical was already active AND the target was the leader surface. Claude's teams protocol targets arbitrary panes from list-panes, not necessarily the leader. Now the first teammate split goes right (creating the column), auto-seeds the main-vertical state, and all subsequent splits stack downward. The caller's surface (CMUX_SURFACE_ID) is used as the anchor regardless of which pane Claude targets. Also adds caller surface preference in pane target resolution so the caller's exact surface is used when the target pane matches, preventing stale selected-surface references after tab switches.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af0290cb87
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if direction == "right", let callerSurface = tmuxCallerSurfaceHandle() { | ||
| // First right split created the column; seed main-vertical | ||
| // state so subsequent splits stack downward. | ||
| updatedStore.mainVerticalLayouts[target.workspaceId] = MainVerticalState( | ||
| mainSurfaceId: callerSurface, |
There was a problem hiding this comment.
Initialize main-vertical state only after layout command
This block creates mainVerticalLayouts on any first right split, even when select-layout main-vertical was never issued. Because later split-window calls check only whether mainVerticalLayouts[wsId] exists, the second split in the workspace is redirected to down unexpectedly, changing normal tmux split behavior and causing unintended vertical stacking in flows that never opted into main-vertical.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
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:9577">
P2: The new workspace fallback can pair `target.workspaceId` with `callerSurface`, creating an invalid cross-workspace `surface.split` request.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| let store = loadTmuxCompatStore() | ||
| if let callerSurface = tmuxCallerSurfaceHandle(), | ||
| let callerWorkspace = tmuxCallerWorkspaceHandle() { | ||
| let wsId = (try? resolveWorkspaceId(callerWorkspace, client: client)) ?? target.workspaceId |
There was a problem hiding this comment.
P2: The new workspace fallback can pair target.workspaceId with callerSurface, creating an invalid cross-workspace surface.split request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 9577:
<comment>The new workspace fallback can pair `target.workspaceId` with `callerSurface`, creating an invalid cross-workspace `surface.split` request.</comment>
<file context>
@@ -9556,17 +9568,23 @@ struct CMUXCLI {
- direction = "down"
+ if let callerSurface = tmuxCallerSurfaceHandle(),
+ let callerWorkspace = tmuxCallerWorkspaceHandle() {
+ let wsId = (try? resolveWorkspaceId(callerWorkspace, client: client)) ?? target.workspaceId
+ if let mvState = store.mainVerticalLayouts[wsId],
+ let lastColumn = mvState.lastColumnSurfaceId {
</file context>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
CLI/cmux.swift (2)
9839-9857:⚠️ Potential issue | 🟠 MajorClear persisted main-vertical state when another layout is selected.
This branch only seeds state for
main-vertical. If the workspace later selects a different layout, the old right-column anchor survives and future splits still get redirected as ifmain-verticalwere active.Suggested fix
case "select-layout": let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: []) + let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) let layoutName = parsed.positional.first ?? "" if layoutName == "main-vertical" { - let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) if let callerSurface = tmuxCallerSurfaceHandle() { var store = loadTmuxCompatStore() // Seed lastColumnSurfaceId from the most recent split if // this is the first time main-vertical is set and a split // already happened (the normal flow: split then layout). @@ ) try saveTmuxCompatStore(store) } + } else { + var store = loadTmuxCompatStore() + store.mainVerticalLayouts.removeValue(forKey: workspaceId) + try saveTmuxCompatStore(store) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 9839 - 9857, When handling the "select-layout" branch, ensure that when the chosen layout is not "main-vertical" you clear any persisted main-vertical state for the resolved workspace so stale lastColumnSurfaceId doesn't affect future splits; after calling tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) and obtaining the workspaceId (same place you currently seed MainVerticalState when layoutName == "main-vertical"), load the store via loadTmuxCompatStore(), remove or nil out store.mainVerticalLayouts[workspaceId] (or delete the entry), and persist the change with try saveTmuxCompatStore(store) so selecting any other layout clears the old right-column anchor.
9606-9614:⚠️ Potential issue | 🟠 MajorOnly advance the right-column anchor when that column actually grew.
Once
mainVerticalLayouts[target.workspaceId]exists, this updateslastColumnSurfaceIdafter every split. A split on some other pane will poison the stored anchor, and the next teammate gets redirected to the wrong surface.Suggested fix
do { var updatedStore = loadTmuxCompatStore() updatedStore.lastSplitSurface[target.workspaceId] = surfaceId - if updatedStore.mainVerticalLayouts[target.workspaceId] != nil { - updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId = surfaceId + if let mvState = updatedStore.mainVerticalLayouts[target.workspaceId], + direction == "down", + target.surfaceId == mvState.lastColumnSurfaceId { + updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId = surfaceId } else if direction == "right", let callerSurface = tmuxCallerSurfaceHandle() { // First right split created the column; seed main-vertical // state so subsequent splits stack downward. updatedStore.mainVerticalLayouts[target.workspaceId] = MainVerticalState(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 9606 - 9614, The current code unconditionally updates updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId on any split once the workspace entry exists, poisoning the right-column anchor; change the update so lastColumnSurfaceId is only advanced when the split actually grew that right-most column — e.g., obtain the caller surface via tmuxCallerSurfaceHandle() (or the split origin), verify the split direction is "right" and that the caller surface is the current lastColumnSurfaceId (or that the new surface belongs to the same right-most column) before assigning lastColumnSurfaceId = surfaceId; keep the existing seeding logic that creates MainVerticalState when the entry is missing.
🤖 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 9603-9616: The load→mutate→save sequence for tmux-compat state
(loadTmuxCompatStore → mutate lastSplitSurface/mainVerticalLayouts →
saveTmuxCompatStore) must be protected with the same lock-file pattern used by
ClaudeHookSessionStore to prevent cross-process races; acquire the lock before
calling loadTmuxCompatStore, perform the mutations (including the
MainVerticalState seeding that uses tmuxCallerSurfaceHandle and updates
lastSplitSurface and
mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId), then
saveTmuxCompatStore and release the lock; apply the same locking change to the
other similar block around lines 9845–9855.
---
Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 9839-9857: When handling the "select-layout" branch, ensure that
when the chosen layout is not "main-vertical" you clear any persisted
main-vertical state for the resolved workspace so stale lastColumnSurfaceId
doesn't affect future splits; after calling
tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client) and obtaining the
workspaceId (same place you currently seed MainVerticalState when layoutName ==
"main-vertical"), load the store via loadTmuxCompatStore(), remove or nil out
store.mainVerticalLayouts[workspaceId] (or delete the entry), and persist the
change with try saveTmuxCompatStore(store) so selecting any other layout clears
the old right-column anchor.
- Around line 9606-9614: The current code unconditionally updates
updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId on any
split once the workspace entry exists, poisoning the right-column anchor; change
the update so lastColumnSurfaceId is only advanced when the split actually grew
that right-most column — e.g., obtain the caller surface via
tmuxCallerSurfaceHandle() (or the split origin), verify the split direction is
"right" and that the caller surface is the current lastColumnSurfaceId (or that
the new surface belongs to the same right-most column) before assigning
lastColumnSurfaceId = surfaceId; keep the existing seeding logic that creates
MainVerticalState when the entry is missing.
🪄 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: a6a9b8e0-cee1-4e45-a7b2-de4796614156
📒 Files selected for processing (2)
CLI/cmux.swiftSources/TerminalController.swift
✅ Files skipped from review due to trivial changes (1)
- Sources/TerminalController.swift
| do { | ||
| var updatedStore = loadTmuxCompatStore() | ||
| updatedStore.lastSplitSurface[target.workspaceId] = surfaceId | ||
| if updatedStore.mainVerticalLayouts[target.workspaceId] != nil { | ||
| updatedStore.mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId = surfaceId | ||
| } else if direction == "right", let callerSurface = tmuxCallerSurfaceHandle() { | ||
| // First right split created the column; seed main-vertical | ||
| // state so subsequent splits stack downward. | ||
| updatedStore.mainVerticalLayouts[target.workspaceId] = MainVerticalState( | ||
| mainSurfaceId: callerSurface, | ||
| lastColumnSurfaceId: surfaceId | ||
| ) | ||
| } | ||
| try saveTmuxCompatStore(updatedStore) |
There was a problem hiding this comment.
Protect tmux-compat store updates with a lock.
These are now cross-process load → mutate → save sequences on shared JSON state. Two overlapping shim invocations can lose lastSplitSurface / lastColumnSurfaceId updates and make later split routing nondeterministic. The lock-file pattern already used by ClaudeHookSessionStore earlier in this file fits here.
Also applies to: 9845-9855
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLI/cmux.swift` around lines 9603 - 9616, The load→mutate→save sequence for
tmux-compat state (loadTmuxCompatStore → mutate
lastSplitSurface/mainVerticalLayouts → saveTmuxCompatStore) must be protected
with the same lock-file pattern used by ClaudeHookSessionStore to prevent
cross-process races; acquire the lock before calling loadTmuxCompatStore,
perform the mutations (including the MainVerticalState seeding that uses
tmuxCallerSurfaceHandle and updates lastSplitSurface and
mainVerticalLayouts[target.workspaceId]?.lastColumnSurfaceId), then
saveTmuxCompatStore and release the lock; apply the same locking change to the
other similar block around lines 9845–9855.
- Keep upstream main-vertical layout anchoring from #2119 - Keep upstream focus param (v2Bool) instead of no_focus - Combine with our -d flag handling: -d sets focus=false - Include customCommands nav item from main
* Add `cmux omo` command for OpenCode + oh-my-openagent integration Same pattern as `cmux claude-teams`: creates a tmux shim so oh-my-openagent's TmuxSessionManager spawns agents as native cmux splits instead of tmux panes. Sets TMUX/TMUX_PANE env vars, prepends shim to PATH, and execs into opencode. Closes #2085 * Auto-install oh-my-opencode plugin when running cmux omo Before launching opencode, cmux omo now: - Checks if oh-my-opencode is registered in ~/.config/opencode/opencode.json - If not, creates/updates the config with the plugin entry - Checks if the npm package is installed in node_modules - If not, runs bun add (or npm install) to install it - Then proceeds with tmux shim setup and exec * Use shadow config dir to avoid modifying user's opencode setup Instead of writing directly to ~/.config/opencode/opencode.json, cmux omo now creates a shadow config at ~/.cmuxterm/omo-config/ that layers oh-my-opencode on top of the user's existing config. Symlinks node_modules, package.json, bun.lock, and plugin config from the original dir. Sets OPENCODE_CONFIG_DIR to the shadow directory. Running plain `opencode` remains unaffected. * Add Agent Integrations docs section with Claude Code Teams and oh-my-opencode pages Adds sectioned sidebar navigation to the docs site. The new Agent Integrations section contains separate pages for cmux claude-teams and cmux omo, documenting usage, tmux shim mechanics, directory layout, environment variables, and the shadow config approach. Both pages include a nightly-only warning. Full English and Japanese translations, nav item keys added to all 19 locales. * Remove uppercase from sidebar section headers * Add more spacing above and below sidebar section headers * Enable tmux mode in oh-my-opencode config, improve docs - cmux omo now writes tmux.enabled=true to the shadow oh-my-opencode.json config. Without this, oh-my-openagent's TmuxSessionManager won't spawn visual panes even though $TMUX is set (the config defaults to false). - Nightly warnings now link to /nightly instead of generic text. - Added "What you get" section to oh-my-opencode docs explaining the visual pane behavior (auto-layout, idle cleanup, queueing). - Added tmux.enabled step to first-run and how-it-works sections. * Add terminal-notifier shim to route oh-my-openagent notifications to cmux oh-my-openagent sends macOS notifications via terminal-notifier (args: -title <t> -message <m> [-activate <id>]). The shim in ~/.cmuxterm/omo-bin/terminal-notifier intercepts these calls and routes them through cmux notify, so notifications appear in cmux's sidebar panel instead of as raw macOS notifications. * Add pane geometry to tmux-compat for oh-my-openagent grid planning oh-my-openagent's TmuxSessionManager needs pane geometry (columns, rows, position, window dimensions) to decide where to spawn agent panes. Without this data, agents run headlessly. Server side: - pane.list v2 response now includes pixel_frame, cell_size, columns, rows per pane, plus container_frame at the top level - Uses BonsplitController.layoutSnapshot() for pixel geometry and ghostty_surface_size() for terminal grid dimensions CLI side: - tmuxEnrichContextWithGeometry() computes character-cell positions from pixel frames and cell dimensions for tmux format variables (pane_width, pane_height, pane_left, pane_top, pane_active, window_width, window_height) - list-panes now resolves pane targets (%uuid) via tmuxResolvePaneTarget instead of failing with "Workspace not found" - display-message enriched with geometry for format strings like #{pane_width},#{window_width} - tmux -V now returns "tmux 3.4" (needed by oh-my-openagent's tmux-path-resolver verification) * Add socket tests for tmux-compat pane geometry 6 tests verifying the geometry enrichment works end-to-end: - pane.list returns pixel_frame, columns, rows, cell_size, container_frame - tmux -V returns version string - list-panes -F renders geometry format variables as integers - list-panes -t %<uuid> resolves pane targets - display -p renders pane_width and window_width - After split, two panes have different positions and halved widths All 6 pass on macmini (cmux-macmini). * Handle tmux -V in shim script directly (no socket needed) oh-my-openagent's tmux-path-resolver runs tmux -V to verify the binary works. The __tmux-compat handler requires a socket connection, which may not be established at verification time. Handle -V in the bash shim directly to avoid the socket dependency. * Lower default tmux pane min widths for cmux omo oh-my-openagent defaults: main_pane_min_width=120, agent_pane_min_width=40, requiring 161+ columns. Most terminal windows are narrower, causing decideSpawnActions to return canSpawn=false and defer agents forever. cmux omo now sets: main_pane_min_width=60, agent_pane_min_width=30, main_pane_size=50, requiring only 91 columns. Also moved tmux -V handling into the bash shim to avoid needing a socket connection for the version check. * Resolve merge conflicts with main (main-vertical layout, focus param) - Keep upstream main-vertical layout anchoring from #2119 - Keep upstream focus param (v2Bool) instead of no_focus - Combine with our -d flag handling: -d sets focus=false - Include customCommands nav item from main * Implement select-layout equalize and resize-pane absolute width When oh-my-openagent spawns agent panes, it calls select-layout main-vertical after each split to redistribute panes evenly, then resize-pane -x <columns> to set the main pane width. Both were previously no-ops, causing cascading uneven splits. Server side: - Add workspace.equalize_splits v2 API that calls the existing TabManager.equalizeSplits (sets all dividers to 0.5) CLI side: - select-layout now calls workspace.equalize_splits before tracking main-vertical state - resize-pane -x <columns> without directional flags now computes the pixel delta from current to desired width and resizes accordingly * Fix equalize to use proportional divider positions The previous equalize set all dividers to 0.5, which in a right- recursive binary tree (from successive splits) gives 50/25/12.5/6.25% instead of equal sizes. New algorithm counts leaf panes on each side of each split and sets the divider to N_left / (N_left + N_right). For 5 panes in a chain: 1/5, 1/4, 1/3, 1/2, giving each pane exactly 20%. * Fix select-layout main-vertical to only equalize vertical splits The proportional equalize was treating the top-level horizontal split (main vs agent column) the same as vertical splits, setting the main pane to 1/6 of the window with 5 agents. For main-vertical layout, only equalize vertical splits (the agent column), leaving the horizontal main/agent divider untouched. The subsequent resize-pane -x handles the main pane width. workspace.equalize_splits now accepts an optional orientation filter ("vertical" or "horizontal") to scope which splits get equalized. * Re-equalize agent column after kill-pane * Address PR review comments - Fix cmux omo --help: remove omo from the help-bypass guard so --help shows usage text instead of trying to launch opencode - Don't overwrite unreadable opencode.json: fail with an error instead of silently resetting to empty config - Drain installer pipes concurrently before waitUntilExit to prevent deadlock from full pipe buffers during bun/npm install --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
…i#2119) * Fix claude-teams pane anchoring: main-vertical layout + focus Claude's agent teams sends `split-window -h` for each teammate then `select-layout main-vertical` to stack them vertically. Three fixes: 1. Implement select-layout main-vertical: track layout state in the tmux compat store so subsequent horizontal splits of the leader pane get redirected to vertical splits of the right-side column. 2. Pass focus:false to surface.split from the split-window handler so internal bonsplit focus stays on the leader pane. 3. Fix tmuxCompatStoreURL to respect $HOME env var (was using NSString.expandingTildeInPath which ignores $HOME). Closes manaflow-ai#2118 * Fix claude-teams split routing: auto-seed main-vertical on first right split The split-window routing was broken in two ways: 1. After the first teammate split (right), the main-vertical state wasn't being created, so all subsequent splits also went right instead of stacking down in the right column. 2. The old code only redirected splits when main-vertical was already active AND the target was the leader surface. Claude's teams protocol targets arbitrary panes from list-panes, not necessarily the leader. Now the first teammate split goes right (creating the column), auto-seeds the main-vertical state, and all subsequent splits stack downward. The caller's surface (CMUX_SURFACE_ID) is used as the anchor regardless of which pane Claude targets. Also adds caller surface preference in pane target resolution so the caller's exact surface is used when the target pane matches, preventing stale selected-surface references after tab switches. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
) * Add `cmux omo` command for OpenCode + oh-my-openagent integration Same pattern as `cmux claude-teams`: creates a tmux shim so oh-my-openagent's TmuxSessionManager spawns agents as native cmux splits instead of tmux panes. Sets TMUX/TMUX_PANE env vars, prepends shim to PATH, and execs into opencode. Closes manaflow-ai#2085 * Auto-install oh-my-opencode plugin when running cmux omo Before launching opencode, cmux omo now: - Checks if oh-my-opencode is registered in ~/.config/opencode/opencode.json - If not, creates/updates the config with the plugin entry - Checks if the npm package is installed in node_modules - If not, runs bun add (or npm install) to install it - Then proceeds with tmux shim setup and exec * Use shadow config dir to avoid modifying user's opencode setup Instead of writing directly to ~/.config/opencode/opencode.json, cmux omo now creates a shadow config at ~/.cmuxterm/omo-config/ that layers oh-my-opencode on top of the user's existing config. Symlinks node_modules, package.json, bun.lock, and plugin config from the original dir. Sets OPENCODE_CONFIG_DIR to the shadow directory. Running plain `opencode` remains unaffected. * Add Agent Integrations docs section with Claude Code Teams and oh-my-opencode pages Adds sectioned sidebar navigation to the docs site. The new Agent Integrations section contains separate pages for cmux claude-teams and cmux omo, documenting usage, tmux shim mechanics, directory layout, environment variables, and the shadow config approach. Both pages include a nightly-only warning. Full English and Japanese translations, nav item keys added to all 19 locales. * Remove uppercase from sidebar section headers * Add more spacing above and below sidebar section headers * Enable tmux mode in oh-my-opencode config, improve docs - cmux omo now writes tmux.enabled=true to the shadow oh-my-opencode.json config. Without this, oh-my-openagent's TmuxSessionManager won't spawn visual panes even though $TMUX is set (the config defaults to false). - Nightly warnings now link to /nightly instead of generic text. - Added "What you get" section to oh-my-opencode docs explaining the visual pane behavior (auto-layout, idle cleanup, queueing). - Added tmux.enabled step to first-run and how-it-works sections. * Add terminal-notifier shim to route oh-my-openagent notifications to cmux oh-my-openagent sends macOS notifications via terminal-notifier (args: -title <t> -message <m> [-activate <id>]). The shim in ~/.cmuxterm/omo-bin/terminal-notifier intercepts these calls and routes them through cmux notify, so notifications appear in cmux's sidebar panel instead of as raw macOS notifications. * Add pane geometry to tmux-compat for oh-my-openagent grid planning oh-my-openagent's TmuxSessionManager needs pane geometry (columns, rows, position, window dimensions) to decide where to spawn agent panes. Without this data, agents run headlessly. Server side: - pane.list v2 response now includes pixel_frame, cell_size, columns, rows per pane, plus container_frame at the top level - Uses BonsplitController.layoutSnapshot() for pixel geometry and ghostty_surface_size() for terminal grid dimensions CLI side: - tmuxEnrichContextWithGeometry() computes character-cell positions from pixel frames and cell dimensions for tmux format variables (pane_width, pane_height, pane_left, pane_top, pane_active, window_width, window_height) - list-panes now resolves pane targets (%uuid) via tmuxResolvePaneTarget instead of failing with "Workspace not found" - display-message enriched with geometry for format strings like #{pane_width},#{window_width} - tmux -V now returns "tmux 3.4" (needed by oh-my-openagent's tmux-path-resolver verification) * Add socket tests for tmux-compat pane geometry 6 tests verifying the geometry enrichment works end-to-end: - pane.list returns pixel_frame, columns, rows, cell_size, container_frame - tmux -V returns version string - list-panes -F renders geometry format variables as integers - list-panes -t %<uuid> resolves pane targets - display -p renders pane_width and window_width - After split, two panes have different positions and halved widths All 6 pass on macmini (cmux-macmini). * Handle tmux -V in shim script directly (no socket needed) oh-my-openagent's tmux-path-resolver runs tmux -V to verify the binary works. The __tmux-compat handler requires a socket connection, which may not be established at verification time. Handle -V in the bash shim directly to avoid the socket dependency. * Lower default tmux pane min widths for cmux omo oh-my-openagent defaults: main_pane_min_width=120, agent_pane_min_width=40, requiring 161+ columns. Most terminal windows are narrower, causing decideSpawnActions to return canSpawn=false and defer agents forever. cmux omo now sets: main_pane_min_width=60, agent_pane_min_width=30, main_pane_size=50, requiring only 91 columns. Also moved tmux -V handling into the bash shim to avoid needing a socket connection for the version check. * Resolve merge conflicts with main (main-vertical layout, focus param) - Keep upstream main-vertical layout anchoring from manaflow-ai#2119 - Keep upstream focus param (v2Bool) instead of no_focus - Combine with our -d flag handling: -d sets focus=false - Include customCommands nav item from main * Implement select-layout equalize and resize-pane absolute width When oh-my-openagent spawns agent panes, it calls select-layout main-vertical after each split to redistribute panes evenly, then resize-pane -x <columns> to set the main pane width. Both were previously no-ops, causing cascading uneven splits. Server side: - Add workspace.equalize_splits v2 API that calls the existing TabManager.equalizeSplits (sets all dividers to 0.5) CLI side: - select-layout now calls workspace.equalize_splits before tracking main-vertical state - resize-pane -x <columns> without directional flags now computes the pixel delta from current to desired width and resizes accordingly * Fix equalize to use proportional divider positions The previous equalize set all dividers to 0.5, which in a right- recursive binary tree (from successive splits) gives 50/25/12.5/6.25% instead of equal sizes. New algorithm counts leaf panes on each side of each split and sets the divider to N_left / (N_left + N_right). For 5 panes in a chain: 1/5, 1/4, 1/3, 1/2, giving each pane exactly 20%. * Fix select-layout main-vertical to only equalize vertical splits The proportional equalize was treating the top-level horizontal split (main vs agent column) the same as vertical splits, setting the main pane to 1/6 of the window with 5 agents. For main-vertical layout, only equalize vertical splits (the agent column), leaving the horizontal main/agent divider untouched. The subsequent resize-pane -x handles the main pane width. workspace.equalize_splits now accepts an optional orientation filter ("vertical" or "horizontal") to scope which splits get equalized. * Re-equalize agent column after kill-pane * Address PR review comments - Fix cmux omo --help: remove omo from the help-bypass guard so --help shows usage text instead of trying to launch opencode - Don't overwrite unreadable opencode.json: fail with an error instead of silently resetting to empty config - Drain installer pipes concurrently before waitUntilExit to prevent deadlock from full pipe buffers during bun/npm install --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Summary
select-layout main-verticalin the tmux compat layer so teammate panes stack vertically in a right-side column instead of creating nested horizontal splitsfocus: falsetosurface.splitfrom thesplit-windowhandler so focus stays on the leader panetmuxCompatStoreURLto respect$HOMEenv varTesting
All claude-teams CLI tests pass:
New test (
test_cli_claude_teams_main_vertical.py) verifies 3 teammates: first splits right of leader, second and third split down from the right column (verified via fake server split_calls recording).Related
Summary by cubic
Teammate panes in Claude Teams now stack vertically in a right-side column when using the main-vertical layout, and focus stays on the leader pane. Also fixes the tmux compat store path to respect the
$HOMEenv var.New Features
select-layout main-verticalin the tmux compat layer with per-workspace state, redirecting subsequent leader right-splits to vertical splits in the right column.Bug Fixes
focus: falsetosurface.splitfrom thesplit-windowhandler to keep focus on the leader pane.tmuxCompatStoreURLto correctly use$HOMEfor the compat store file path.Written for commit d140f29. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements
Tests