Merge pull request #925 from jleechanorg/fix-split-cwd-inheritance
Fix split CWD inheritance and bash job notification spam
This commit is contained in:
commit
b848d60b0d
4 changed files with 229 additions and 3 deletions
|
|
@ -100,7 +100,7 @@ _cmux_report_tty_once() {
|
|||
_CMUX_TTY_REPORTED=1
|
||||
{
|
||||
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &
|
||||
} >/dev/null 2>&1 & disown
|
||||
}
|
||||
|
||||
_cmux_ports_kick() {
|
||||
|
|
@ -112,7 +112,7 @@ _cmux_ports_kick() {
|
|||
_CMUX_PORTS_LAST_RUN=$SECONDS
|
||||
{
|
||||
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &
|
||||
} >/dev/null 2>&1 & disown
|
||||
}
|
||||
|
||||
_cmux_prompt_command() {
|
||||
|
|
@ -161,7 +161,7 @@ _cmux_prompt_command() {
|
|||
{
|
||||
local qpwd="${pwd//\"/\\\"}"
|
||||
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &
|
||||
} >/dev/null 2>&1 & disown
|
||||
fi
|
||||
|
||||
# Branch can change via aliases/tools while an older probe is still in flight.
|
||||
|
|
@ -211,6 +211,7 @@ _cmux_prompt_command() {
|
|||
fi
|
||||
} >/dev/null 2>&1 &
|
||||
_CMUX_GIT_JOB_PID=$!
|
||||
disown
|
||||
_CMUX_GIT_JOB_STARTED_AT=$now
|
||||
fi
|
||||
|
||||
|
|
@ -254,6 +255,7 @@ _cmux_prompt_command() {
|
|||
fi
|
||||
} >/dev/null 2>&1 &
|
||||
_CMUX_PR_JOB_PID=$!
|
||||
disown
|
||||
_CMUX_PR_JOB_STARTED_AT=$now
|
||||
fi
|
||||
fi
|
||||
|
|
@ -262,6 +264,7 @@ _cmux_prompt_command() {
|
|||
if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then
|
||||
_cmux_ports_kick
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
_cmux_install_prompt_command() {
|
||||
|
|
|
|||
|
|
@ -1956,11 +1956,21 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
guard let paneId = sourcePaneId else { return nil }
|
||||
let inheritedConfig = inheritedTerminalConfig(preferredPanelId: panelId, inPane: paneId)
|
||||
|
||||
// Inherit working directory: prefer the source panel's reported cwd,
|
||||
// fall back to the workspace's current directory.
|
||||
let splitWorkingDirectory: String? = panelDirectories[panelId]
|
||||
?? (currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
? nil : currentDirectory)
|
||||
#if DEBUG
|
||||
dlog("split.cwd panelId=\(panelId.uuidString.prefix(5)) panelDir=\(panelDirectories[panelId] ?? "nil") currentDir=\(currentDirectory) resolved=\(splitWorkingDirectory ?? "nil")")
|
||||
#endif
|
||||
|
||||
// Create the new terminal panel.
|
||||
let newPanel = TerminalPanel(
|
||||
workspaceId: id,
|
||||
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
|
||||
configTemplate: inheritedConfig,
|
||||
workingDirectory: splitWorkingDirectory,
|
||||
portOrdinal: portOrdinal
|
||||
)
|
||||
panels[newPanel.id] = newPanel
|
||||
|
|
|
|||
BIN
docs/assets/split-cwd-inheritance-demo.gif
Normal file
BIN
docs/assets/split-cwd-inheritance-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 829 KiB |
213
tests/test_split_cwd_inheritance.py
Normal file
213
tests/test_split_cwd_inheritance.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
End-to-end test for split CWD inheritance.
|
||||
|
||||
Verifies that new split panes and new workspace tabs inherit the current
|
||||
working directory from the source terminal.
|
||||
|
||||
Requires:
|
||||
- cmux running with allowAll socket mode
|
||||
- bash shell integration sourced (cmux-bash-integration.bash)
|
||||
|
||||
Run with a tagged instance:
|
||||
CMUX_TAG=<tag> python3 tests/test_split_cwd_inheritance.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from cmux import cmux # noqa: E402
|
||||
|
||||
|
||||
def _parse_sidebar_state(text: str) -> dict[str, str]:
|
||||
data: dict[str, str] = {}
|
||||
for raw in (text or "").splitlines():
|
||||
line = raw.rstrip("\n")
|
||||
if not line or line.startswith(" "):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
data[k.strip()] = v.strip()
|
||||
return data
|
||||
|
||||
|
||||
def _wait_for(predicate, timeout: float, interval: float, label: str):
|
||||
start = time.time()
|
||||
last_error: Exception | None = None
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
value = predicate()
|
||||
if value:
|
||||
return value
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
time.sleep(interval)
|
||||
extra = ""
|
||||
if last_error is not None:
|
||||
extra = f" Last error: {last_error}"
|
||||
raise AssertionError(f"Timed out waiting for {label}.{extra}")
|
||||
|
||||
|
||||
def _wait_for_focused_cwd(
|
||||
client: cmux,
|
||||
expected: str,
|
||||
timeout: float = 12.0,
|
||||
exclude_panel: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Wait for focused_cwd to match expected.
|
||||
|
||||
If exclude_panel is given, also require that focused_panel differs from
|
||||
that value — ensuring we're checking the *new* pane, not the original.
|
||||
"""
|
||||
def pred():
|
||||
state = _parse_sidebar_state(client.sidebar_state())
|
||||
cwd = state.get("focused_cwd", "")
|
||||
if cwd != expected:
|
||||
return None
|
||||
if exclude_panel and state.get("focused_panel", "") == exclude_panel:
|
||||
return None
|
||||
return state
|
||||
label = f"focused_cwd={expected!r}"
|
||||
if exclude_panel:
|
||||
label += f" (panel != {exclude_panel})"
|
||||
return _wait_for(pred, timeout=timeout, interval=0.3, label=label)
|
||||
|
||||
|
||||
def _send_cd_and_wait(
|
||||
client: cmux,
|
||||
target: str,
|
||||
timeout: float = 12.0,
|
||||
) -> dict[str, str]:
|
||||
"""cd to target and wait for sidebar focused_cwd to reflect it."""
|
||||
client.send(f"cd {target}\n")
|
||||
return _wait_for_focused_cwd(client, target, timeout=timeout)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tag = os.environ.get("CMUX_TAG", "")
|
||||
|
||||
socket_path = None
|
||||
if tag:
|
||||
socket_path = f"/tmp/cmux-debug-{tag}.sock"
|
||||
client = cmux(socket_path=socket_path)
|
||||
client.connect()
|
||||
|
||||
# Use resolved paths to avoid /tmp -> /private/tmp symlink mismatch on macOS
|
||||
test_dir_a = str(Path("/tmp/cmux_split_cwd_test_a").resolve())
|
||||
test_dir_b = str(Path("/tmp/cmux_split_cwd_test_b").resolve())
|
||||
os.makedirs(test_dir_a, exist_ok=True)
|
||||
os.makedirs(test_dir_b, exist_ok=True)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name: str, condition: bool, detail: str = ""):
|
||||
nonlocal passed, failed
|
||||
if condition:
|
||||
print(f" PASS {name}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL {name}{': ' + detail if detail else ''}")
|
||||
failed += 1
|
||||
|
||||
print("=== Split CWD Inheritance Tests ===")
|
||||
|
||||
# --- Setup: cd to test_dir_a in workspace 1 ---
|
||||
print(" [setup] cd to test_dir_a and wait for shell integration...")
|
||||
_send_cd_and_wait(client, test_dir_a)
|
||||
state = _parse_sidebar_state(client.sidebar_state())
|
||||
check("setup: focused_cwd is test_dir_a", state.get("focused_cwd") == test_dir_a,
|
||||
f"got {state.get('focused_cwd')!r}")
|
||||
|
||||
# --- Test 1: New split inherits test_dir_a ---
|
||||
print(" [test1] creating right split from test_dir_a...")
|
||||
# Record the original panel so we can verify focus moves to the NEW pane.
|
||||
original_panel = state.get("focused_panel", "")
|
||||
split_result = client.new_split("right")
|
||||
if not split_result:
|
||||
check("split created", False)
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
client.close()
|
||||
return 1
|
||||
check("split created", True)
|
||||
|
||||
# Wait for the NEW pane (different panel ID) to report test_dir_a.
|
||||
time.sleep(4) # wait for new bash to start + run PROMPT_COMMAND
|
||||
try:
|
||||
state = _wait_for_focused_cwd(
|
||||
client, test_dir_a, timeout=15.0, exclude_panel=original_panel,
|
||||
)
|
||||
new_panel = state.get("focused_panel", "")
|
||||
check("test1: focus moved to new pane", new_panel != original_panel,
|
||||
f"original={original_panel!r}, current={new_panel!r}")
|
||||
check("test1: split inherited test_dir_a",
|
||||
state.get("focused_cwd") == test_dir_a,
|
||||
f"focused_cwd={state.get('focused_cwd')!r}")
|
||||
except AssertionError:
|
||||
state = _parse_sidebar_state(client.sidebar_state())
|
||||
check("test1: split inherited test_dir_a", False,
|
||||
f"focused_cwd={state.get('focused_cwd')!r}, focused_panel={state.get('focused_panel')!r}")
|
||||
|
||||
# --- Test 2: New workspace tab inherits CWD ---
|
||||
# First cd to test_dir_b so we have a different dir to inherit
|
||||
print(" [test2] cd to test_dir_b, then creating new workspace tab...")
|
||||
_send_cd_and_wait(client, test_dir_b)
|
||||
state = _parse_sidebar_state(client.sidebar_state())
|
||||
original_tab = state.get("tab", "")
|
||||
|
||||
tab_result = client.new_tab()
|
||||
if not tab_result:
|
||||
check("new tab created", False)
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
client.close()
|
||||
return 1
|
||||
check("new tab created", True)
|
||||
|
||||
# New workspace should be a different tab AND inherit test_dir_b
|
||||
time.sleep(4)
|
||||
try:
|
||||
def _new_tab_with_cwd():
|
||||
s = _parse_sidebar_state(client.sidebar_state())
|
||||
tab_id = s.get("tab", "")
|
||||
cwd = s.get("focused_cwd", "")
|
||||
if tab_id != original_tab and cwd == test_dir_b:
|
||||
return s
|
||||
return None
|
||||
|
||||
state = _wait_for(
|
||||
_new_tab_with_cwd, timeout=15.0, interval=0.3,
|
||||
label=f"new tab with focused_cwd={test_dir_b!r}",
|
||||
)
|
||||
check("test2: focus moved to new tab", state.get("tab") != original_tab,
|
||||
f"original={original_tab!r}, current={state.get('tab')!r}")
|
||||
check("test2: new workspace inherited test_dir_b",
|
||||
state.get("focused_cwd") == test_dir_b,
|
||||
f"focused_cwd={state.get('focused_cwd')!r}")
|
||||
except AssertionError:
|
||||
state = _parse_sidebar_state(client.sidebar_state())
|
||||
check("test2: new workspace inherited test_dir_b", False,
|
||||
f"focused_cwd={state.get('focused_cwd')!r}, tab={state.get('tab')!r}")
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
|
||||
client.close()
|
||||
|
||||
# Cleanup
|
||||
for d in [test_dir_a, test_dir_b]:
|
||||
try:
|
||||
os.rmdir(d)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue