Compare commits

...
Author SHA1 Message Date
austinpower1258 a67a334437 fix: clarify remote pwd ack contract 2026-06-06 03:16:15 -07:00
austinpower1258 8b6a59a00b fix: acknowledge accepted remote pwd reports 2026-06-06 03:13:06 -07:00
austinpower1258 831dd44fcc fix: avoid acking pending relay pwd 2026-06-06 03:07:00 -07:00
austinpower1258 93806b7eca fix: preserve remote fork cwd and retry relay pwd 2026-06-06 03:02:09 -07:00
austinpower1258 ea60a6eadc fix: preserve exact remote git probe paths 2026-06-06 02:55:35 -07:00
austinpower1258 d5a2dda37d fix: preserve exact remote startup cwd 2026-06-06 02:47:02 -07:00
austinpower1258 14a4ef1b82 fix: resolve report pwd manager on main 2026-06-06 02:38:57 -07:00
austinpower1258 8f5e487be4 fix: propagate remote initial cwd through pty bootstrap 2026-06-06 02:35:12 -07:00
austinpower1258 6097667666 fix: merge remote startup environment paths 2026-06-06 02:23:37 -07:00
austinpower1258 a976bd919a merge: resolve conflicts with main 2026-06-06 02:18:17 -07:00
austinpower1258 ba5cc9a915 fix: keep report pwd path nonblocking 2026-05-26 00:49:28 -07:00
austinpower1258 ef935bff3f Merge remote-tracking branch 'origin/main' into issue-559-ssh-integration 2026-05-26 00:29:16 -07:00
austinpower1258 7817c4513c merge: resolve conflicts with main 2026-05-23 20:22:04 -07:00
austinpower1258 5044f84aba Avoid blocking SSH relay cwd reports 2026-05-12 11:33:36 -07:00
austinpower1258 4d0d50b3e0 Merge remote-tracking branch 'origin/main' into issue-559-ssh-integration 2026-05-12 11:23:56 -07:00
austinpower1258 0d76554751 Improve SSH workspace cwd integration 2026-05-12 11:22:40 -07:00
12 changed files with 1133 additions and 56 deletions
+20 -2
View File
@@ -8161,16 +8161,19 @@ struct CMUXCLI {
var lines: [String] = [
"cmux_workspace_id=\"${CMUX_WORKSPACE_ID:-}\"",
"cmux_surface_id=\"${CMUX_SURFACE_ID:-}\"",
"cmux_remote_initial_cwd=\"${CMUX_REMOTE_INITIAL_CWD:-}\"",
"cmux_remote_initial_cwd_b64=\"\"",
"if [ -n \"$cmux_remote_initial_cwd\" ]; then cmux_remote_initial_cwd_b64=\"$(printf '%s' \"$cmux_remote_initial_cwd\" | base64 | tr -d '\\n')\"; fi",
"cmux_remote_bootstrap_b64=\(shellQuote(encodedBootstrapScript))",
"cmux_remote_bootstrap=\"$(printf %s \"$cmux_remote_bootstrap_b64\" | base64 -d 2>/dev/null || printf %s \"$cmux_remote_bootstrap_b64\" | base64 -D 2>/dev/null)\"",
"cmux_remote_bootstrap=\"$(printf '%s' \"$cmux_remote_bootstrap\" | sed \"s/__CMUX_WORKSPACE_ID__/$cmux_workspace_id/g; s/__CMUX_SURFACE_ID__/$cmux_surface_id/g\")\"",
"cmux_remote_bootstrap=\"$(printf '%s' \"$cmux_remote_bootstrap\" | sed \"s|__CMUX_WORKSPACE_ID__|$cmux_workspace_id|g; s|__CMUX_SURFACE_ID__|$cmux_surface_id|g; s|__CMUX_REMOTE_INITIAL_CWD_B64__|$cmux_remote_initial_cwd_b64|g\")\"",
"printf '%s' \"$cmux_remote_bootstrap\" | command \(installSSHPrefix) -T \(shellQuote(options.destination)) \(shellQuote(remoteBootstrapInstallCommand))",
"cmux_remote_install_status=$?",
"if [ \"$cmux_remote_install_status\" -ne 0 ]; then",
" exit \"$cmux_remote_install_status\"",
"fi",
"cmux_remote_command_template=\(shellQuote(remoteCommandTemplate))",
"cmux_remote_command=\"$(printf '%s' \"$cmux_remote_command_template\" | sed \"s/__CMUX_WORKSPACE_ID__/$cmux_workspace_id/g; s/__CMUX_SURFACE_ID__/$cmux_surface_id/g\")\"",
"cmux_remote_command=\"$(printf '%s' \"$cmux_remote_command_template\" | sed \"s|__CMUX_WORKSPACE_ID__|$cmux_workspace_id|g; s|__CMUX_SURFACE_ID__|$cmux_surface_id|g; s|__CMUX_REMOTE_INITIAL_CWD_B64__|$cmux_remote_initial_cwd_b64|g\")\"",
]
var sshInvocation = "command \(sessionSSHPrefix) -o \"RemoteCommand=$cmux_remote_command\""
@@ -8324,6 +8327,13 @@ struct CMUXCLI {
commonShellExportLines.append(contentsOf: [
"hash -r >/dev/null 2>&1 || true",
"rehash >/dev/null 2>&1 || true",
"cmux_remote_initial_cwd_b64='__CMUX_REMOTE_INITIAL_CWD_B64__'",
"if [ \"$cmux_remote_initial_cwd_b64\" = '__CMUX_''REMOTE_INITIAL_CWD_B64__' ]; then cmux_remote_initial_cwd_b64=''; fi",
"if [ -n \"$cmux_remote_initial_cwd_b64\" ]; then",
" cmux_remote_initial_cwd=\"$(printf %s \"$cmux_remote_initial_cwd_b64\" | base64 -d 2>/dev/null || printf %s \"$cmux_remote_initial_cwd_b64\" | base64 -D 2>/dev/null || true)\"",
" if [ -n \"$cmux_remote_initial_cwd\" ]; then cd \"$cmux_remote_initial_cwd\" 2>/dev/null || true; fi",
"fi",
"unset cmux_remote_initial_cwd_b64 cmux_remote_initial_cwd",
])
var zshShellLines = commonShellExportLines
zshShellLines.append(
@@ -10115,6 +10125,13 @@ struct CMUXCLI {
let explicitAttachmentID = Self.normalizedEnvValue(attachmentIDOpt)
let surfaceID = environmentSurfaceID ?? (explicitAttachmentID.flatMap { UUID(uuidString: $0) == nil ? nil : $0 })
let attachmentID = explicitAttachmentID ?? environmentSurfaceID ?? UUID().uuidString.lowercased()
let remoteInitialCWDB64: String
if let remoteInitialCWD = ProcessInfo.processInfo.environment["CMUX_REMOTE_INITIAL_CWD"],
!remoteInitialCWD.isEmpty {
remoteInitialCWDB64 = Data(remoteInitialCWD.utf8).base64EncodedString()
} else {
remoteInitialCWDB64 = ""
}
let command: String? = try commandB64Opt.flatMap { encoded in
guard let data = Data(base64Encoded: encoded),
var decoded = String(data: data, encoding: .utf8) else {
@@ -10126,6 +10143,7 @@ struct CMUXCLI {
of: "__CMUX_SURFACE_ID__",
with: ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] ?? ""
)
.replacingOccurrences(of: "__CMUX_REMOTE_INITIAL_CWD_B64__", with: remoteInitialCWDB64)
return decoded
}
var bridgeReachedReady = false
@@ -107,6 +107,35 @@ _cmux_relay_rpc_bg() {
_cmux_detach_bg "$relay_cli" rpc "$method" "$params"
}
_cmux_relay_pwd_response_accepted() {
local response="$1"
[[ "$response" == *'"ok":false'* || "$response" == *'"ok": false'* ]] && return 1
[[ "$response" == *'"accepted":false'* || "$response" == *'"accepted": false'* ]] && return 1
[[ "$response" == *'"pending":true'* || "$response" == *'"pending": true'* ]] && return 1
[[ "$response" == *'"accepted":true'* || "$response" == *'"accepted": true'* ]] && return 0
return 0
}
_cmux_relay_rpc_bg_ack_pwd() {
local method="$1"
local params="$2"
local pwd="$3"
local ack_file="$4"
local relay_cli=""
local response=""
_cmux_socket_uses_remote_relay || return 1
[[ -n "$ack_file" ]] || return 1
relay_cli="$(_cmux_relay_cli_path)" || return 1
(
response="$("$relay_cli" rpc "$method" "$params" 2>/dev/null)" || exit 0
response="${response//$'\n'/}"
response="${response//$'\r'/}"
if _cmux_relay_pwd_response_accepted "$response"; then
printf '%s\n' "$pwd" > "$ack_file" 2>/dev/null || true
fi
) >/dev/null 2>&1 &
}
_cmux_relay_rpc() {
local method="$1"
local params="$2"
@@ -162,6 +191,35 @@ _cmux_ports_kick_via_relay() {
_cmux_relay_rpc_bg "surface.ports_kick" "$params"
}
_cmux_report_pwd_via_relay() {
local pwd="$1"
_cmux_socket_uses_remote_relay || return 1
[[ -n "$pwd" ]] || return 1
local workspace_id=""
workspace_id="$(_cmux_relay_workspace_id)" || return 1
local surface_id="${CMUX_PANEL_ID:-${CMUX_SURFACE_ID:-}}"
[[ -n "$surface_id" ]] || return 1
local pwd_json params
pwd_json="$(_cmux_json_escape "$pwd")"
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$surface_id\",\"directory\":\"$pwd_json\"}"
_cmux_relay_rpc_bg_ack_pwd "surface.report_pwd" "$params" "$pwd" "$_CMUX_PWD_RELAY_ACK_FILE"
}
_cmux_apply_relay_pwd_ack() {
local ack_file="${_CMUX_PWD_RELAY_ACK_FILE:-}"
local ack=""
[[ -n "$ack_file" && -r "$ack_file" ]] || return 0
IFS= read -r ack < "$ack_file" || ack=""
/bin/rm -f -- "$ack_file" >/dev/null 2>&1 || true
[[ -n "$ack" ]] || return 0
_CMUX_PWD_LAST_PWD="$ack"
if [[ "${_CMUX_PWD_RELAY_PENDING_PWD:-}" == "$ack" ]]; then
_CMUX_PWD_RELAY_PENDING_PWD=""
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
fi
}
_cmux_restore_scrollback_once() {
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
[[ -n "$path" ]] || return 0
@@ -208,6 +266,10 @@ _cmux_now() {
# Throttle heavy work to avoid prompt latency.
_CMUX_PWD_LAST_PWD="${_CMUX_PWD_LAST_PWD:-}"
_CMUX_PWD_RELAY_ACK_FILE="${_CMUX_PWD_RELAY_ACK_FILE:-${TMPDIR:-/tmp}/cmux-pwd-relay-ack-$$}"
_CMUX_PWD_RELAY_PENDING_PWD="${_CMUX_PWD_RELAY_PENDING_PWD:-}"
_CMUX_PWD_RELAY_PENDING_STARTED_AT="${_CMUX_PWD_RELAY_PENDING_STARTED_AT:-0}"
_CMUX_PWD_RELAY_RETRY_INTERVAL="${_CMUX_PWD_RELAY_RETRY_INTERVAL:-2}"
_CMUX_GIT_LAST_PWD="${_CMUX_GIT_LAST_PWD:-}"
_CMUX_GIT_LAST_RUN="${_CMUX_GIT_LAST_RUN:-0}"
_CMUX_GIT_JOB_PID="${_CMUX_GIT_JOB_PID:-}"
@@ -1381,7 +1443,28 @@ _cmux_prompt_command() {
local now
now="$(_cmux_now)"
local pwd="$PWD"
if (( ! cmux_has_unix_socket )); then
_cmux_apply_relay_pwd_ack
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
local should_report_pwd=1
local pending_started="${_CMUX_PWD_RELAY_PENDING_STARTED_AT:-0}"
local retry_interval="${_CMUX_PWD_RELAY_RETRY_INTERVAL:-2}"
case "$pending_started" in
''|*[!0-9]*) pending_started=0 ;;
esac
case "$retry_interval" in
''|*[!0-9]*) retry_interval=2 ;;
esac
if [[ "$pwd" == "${_CMUX_PWD_RELAY_PENDING_PWD:-}" ]] &&
(( now - pending_started < retry_interval )); then
should_report_pwd=0
fi
if (( should_report_pwd )) && _cmux_report_pwd_via_relay "$pwd"; then
_CMUX_PWD_RELAY_PENDING_PWD="$pwd"
_CMUX_PWD_RELAY_PENDING_STARTED_AT="$now"
fi
fi
if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then
_cmux_ports_kick refresh
fi
@@ -1389,7 +1472,6 @@ _cmux_prompt_command() {
fi
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local pwd="$PWD"
_cmux_set_git_active_pwd "$pwd"
# Post-wake socket writes can occasionally leave a probe process wedged.
@@ -109,6 +109,38 @@ _cmux_relay_rpc_bg() {
{ "$relay_cli" rpc "$method" "$params" >/dev/null 2>&1 || true } >/dev/null 2>&1 &!
}
_cmux_relay_pwd_response_accepted() {
local response="$1"
[[ "$response" == *'"ok":false'* || "$response" == *'"ok": false'* ]] && return 1
[[ "$response" == *'"accepted":false'* || "$response" == *'"accepted": false'* ]] && return 1
[[ "$response" == *'"pending":true'* || "$response" == *'"pending": true'* ]] && return 1
[[ "$response" == *'"accepted":true'* || "$response" == *'"accepted": true'* ]] && return 0
return 0
}
_cmux_relay_rpc_bg_ack_pwd() {
local method="$1"
local params="$2"
local pwd="$3"
local ack_file="$4"
local relay_cli=""
local response=""
local response_status=0
_cmux_zsh_job_table_saturated && return 1
_cmux_socket_uses_remote_relay || return 1
[[ -n "$ack_file" ]] || return 1
relay_cli="$(_cmux_relay_cli_path)" || return 1
{
response="$("$relay_cli" rpc "$method" "$params" 2>/dev/null)"
response_status="$?"
response="${response//$'\n'/}"
response="${response//$'\r'/}"
if (( response_status == 0 )) && _cmux_relay_pwd_response_accepted "$response"; then
print -r -- "$pwd" >| "$ack_file" 2>/dev/null || true
fi
} >/dev/null 2>&1 &!
}
_cmux_relay_rpc() {
local method="$1"
local params="$2"
@@ -164,6 +196,35 @@ _cmux_ports_kick_via_relay() {
_cmux_relay_rpc_bg "surface.ports_kick" "$params"
}
_cmux_report_pwd_via_relay() {
local pwd="$1"
_cmux_socket_uses_remote_relay || return 1
[[ -n "$pwd" ]] || return 1
local workspace_id=""
workspace_id="$(_cmux_relay_workspace_id)" || return 1
local surface_id="${CMUX_PANEL_ID:-${CMUX_SURFACE_ID:-}}"
[[ -n "$surface_id" ]] || return 1
local pwd_json params
pwd_json="$(_cmux_json_escape "$pwd")"
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$surface_id\",\"directory\":\"$pwd_json\"}"
_cmux_relay_rpc_bg_ack_pwd "surface.report_pwd" "$params" "$pwd" "$_CMUX_PWD_RELAY_ACK_FILE"
}
_cmux_apply_relay_pwd_ack() {
local ack_file="${_CMUX_PWD_RELAY_ACK_FILE:-}"
local ack=""
[[ -n "$ack_file" && -r "$ack_file" ]] || return 0
IFS= read -r ack < "$ack_file" || ack=""
/bin/rm -f -- "$ack_file" >/dev/null 2>&1 || true
[[ -n "$ack" ]] || return 0
_CMUX_PWD_LAST_PWD="$ack"
if [[ "${_CMUX_PWD_RELAY_PENDING_PWD:-}" == "$ack" ]]; then
_CMUX_PWD_RELAY_PENDING_PWD=""
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
fi
}
_cmux_restore_scrollback_once() {
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
[[ -n "$path" ]] || return 0
@@ -230,6 +291,10 @@ _cmux_normalize_claude_config_dir
# Throttle heavy work to avoid prompt latency.
typeset -g _CMUX_PWD_LAST_PWD=""
typeset -g _CMUX_PWD_RELAY_ACK_FILE="${TMPDIR:-/tmp}/cmux-pwd-relay-ack-$$"
typeset -g _CMUX_PWD_RELAY_PENDING_PWD=""
typeset -g _CMUX_PWD_RELAY_PENDING_STARTED_AT=0
typeset -g _CMUX_PWD_RELAY_RETRY_INTERVAL=2
typeset -g _CMUX_GIT_LAST_PWD=""
typeset -g _CMUX_GIT_LAST_RUN=0
typeset -g _CMUX_GIT_JOB_PID=""
@@ -1573,7 +1638,29 @@ _cmux_precmd() {
cmd_dur=$(( now - cmd_start ))
fi
local pwd="$PWD"
if (( ! cmux_has_unix_socket )); then
_cmux_apply_relay_pwd_ack
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
local should_report_pwd=1
local pending_started="${_CMUX_PWD_RELAY_PENDING_STARTED_AT:-0}"
local retry_interval="${_CMUX_PWD_RELAY_RETRY_INTERVAL:-2}"
case "$pending_started" in
''|*[!0-9]*) pending_started=0 ;;
esac
case "$retry_interval" in
''|*[!0-9]*) retry_interval=2 ;;
esac
if [[ "$pwd" == "${_CMUX_PWD_RELAY_PENDING_PWD:-}" ]] &&
(( now - pending_started < retry_interval )); then
should_report_pwd=0
fi
if (( should_report_pwd )) && _cmux_report_pwd_via_relay "$pwd"; then
_CMUX_PWD_RELAY_PENDING_PWD="$pwd"
_CMUX_PWD_RELAY_PENDING_STARTED_AT="$now"
fi
fi
if (( cmd_dur >= 2 || now - _CMUX_PORTS_LAST_RUN >= 10 )); then
_cmux_ports_kick refresh
fi
@@ -1581,7 +1668,6 @@ _cmux_precmd() {
fi
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local pwd="$PWD"
_cmux_set_git_active_pwd "$pwd"
_cmux_prompt_wrap_guard "$cmd_start" "$pwd"
@@ -177,12 +177,27 @@ enum RemoteInteractiveShellBootstrapBuilder {
"cmux_surface_id='__CMUX_SURFACE_ID__'",
"case \"$cmux_surface_id\" in \"\"|'__CMUX_''SURFACE_ID__') ;; *) export CMUX_SURFACE_ID=\"$cmux_surface_id\"; export CMUX_PANEL_ID=\"$cmux_surface_id\" ;; esac",
"unset cmux_workspace_id cmux_surface_id",
])
lines.append(contentsOf: remoteInitialWorkingDirectoryLines())
lines.append(contentsOf: [
"hash -r >/dev/null 2>&1 || true",
"rehash >/dev/null 2>&1 || true",
])
return lines
}
private static func remoteInitialWorkingDirectoryLines() -> [String] {
[
"cmux_remote_initial_cwd_b64='__CMUX_REMOTE_INITIAL_CWD_B64__'",
"if [ \"$cmux_remote_initial_cwd_b64\" = '__CMUX_''REMOTE_INITIAL_CWD_B64__' ]; then cmux_remote_initial_cwd_b64=''; fi",
"if [ -n \"$cmux_remote_initial_cwd_b64\" ]; then",
" cmux_remote_initial_cwd=\"$(printf %s \"$cmux_remote_initial_cwd_b64\" | base64 -d 2>/dev/null || printf %s \"$cmux_remote_initial_cwd_b64\" | base64 -D 2>/dev/null || true)\"",
" if [ -n \"$cmux_remote_initial_cwd\" ]; then cd \"$cmux_remote_initial_cwd\" 2>/dev/null || true; fi",
"fi",
"unset cmux_remote_initial_cwd_b64 cmux_remote_initial_cwd",
]
}
private static func terminalSetupLines(terminfoSource: String?) -> [String] {
var lines: [String] = [
"cmux_term='xterm-256color'",
+27 -8
View File
@@ -2279,7 +2279,7 @@ class TabManager: ObservableObject {
let rawDirectory = workspace.panelDirectories[panelId]
?? workspace.terminalPanel(for: panelId)?.requestedWorkingDirectory
?? (workspace.focusedPanelId == panelId ? workspace.currentDirectory : nil)
return rawDirectory.flatMap(normalizedWorkingDirectory)
return gitProbeDirectoryValue(rawDirectory, preserveExact: workspace.isRemoteWorkspace)
}
func scheduleInitialWorkspaceGitMetadataRefreshIfPossible(
@@ -2812,7 +2812,6 @@ class TabManager: ObservableObject {
delays: [TimeInterval],
reason: String
) {
let normalizedDirectory = normalizeDirectory(directory)
let key = WorkspaceGitProbeKey(workspaceId: workspaceId, panelId: panelId)
cancelWorkspaceGitProbeTask(for: key)
if workspaceGitProbeStateByKey[key] == nil {
@@ -2822,7 +2821,7 @@ class TabManager: ObservableObject {
#if DEBUG
cmuxDebugLog(
"workspace.gitProbe.schedule workspace=\(workspaceId.uuidString.prefix(5)) " +
"panel=\(panelId.uuidString.prefix(5)) dir=\(normalizedDirectory) reason=\(reason)"
"panel=\(panelId.uuidString.prefix(5)) dir=\(directory) reason=\(reason)"
)
#endif
@@ -2843,7 +2842,7 @@ class TabManager: ObservableObject {
guard let self, !Task.isCancelled else { return }
self.beginWorkspaceGitMetadataProbeAttempt(
probeKey: key,
expectedDirectory: normalizedDirectory,
expectedDirectory: directory,
isLastAttempt: isLastAttempt
)
}
@@ -3522,6 +3521,14 @@ class TabManager: ObservableObject {
return trimmed.isEmpty ? nil : normalized
}
private func gitProbeDirectoryValue(_ directory: String?, preserveExact: Bool) -> String? {
guard let directory else { return nil }
if preserveExact {
return directory.isEmpty ? nil : directory
}
return normalizedWorkingDirectory(directory)
}
private func newTabInsertIndex(placementOverride: NewWorkspacePlacement? = nil) -> Int {
newTabInsertIndex(snapshot: workspaceCreationSnapshot(), placementOverride: placementOverride)
}
@@ -5002,12 +5009,24 @@ class TabManager: ObservableObject {
// MARK: - Surface Directory Updates (Backwards Compatibility)
func updateSurfaceDirectory(tabId: UUID, surfaceId: UUID, directory: String) {
func updateSurfaceDirectory(
tabId: UUID,
surfaceId: UUID,
directory: String,
preserveExactDirectory: Bool = false
) {
guard let tab = tabs.first(where: { $0.id == tabId }) else { return }
let previousDirectory = gitProbeDirectory(for: tab, panelId: surfaceId)
let normalized = normalizeDirectory(directory)
guard tab.updatePanelDirectory(panelId: surfaceId, directory: normalized) else { return }
let nextDirectory = normalizedWorkingDirectory(normalized)
let normalized = preserveExactDirectory ? directory : normalizeDirectory(directory)
guard tab.updatePanelDirectory(
panelId: surfaceId,
directory: normalized,
preserveExactDirectory: preserveExactDirectory
) else { return }
let nextDirectory = gitProbeDirectoryValue(
normalized,
preserveExact: preserveExactDirectory || tab.isRemoteWorkspace
)
if previousDirectory != nextDirectory {
guard sidebarGitMetadataWatchEnabled else {
clearWorkspaceGitMetadata(for: WorkspaceGitProbeKey(workspaceId: tabId, panelId: surfaceId))
+83
View File
@@ -2028,6 +2028,8 @@ class TerminalController {
return v2Result(id: id, self.v2SurfaceSendKey(params: params))
case "surface.report_tty":
return v2Result(id: id, self.v2SurfaceReportTTY(params: params))
case "surface.report_pwd":
return v2Result(id: id, self.v2SurfaceReportPwd(params: params))
case "surface.report_shell_state":
return v2Result(id: id, self.v2SurfaceReportShellState(params: params))
case "surface.ports_kick":
@@ -2479,6 +2481,7 @@ class TerminalController {
"surface.send_text",
"surface.send_key",
"surface.report_tty",
"surface.report_pwd",
"surface.report_shell_state",
"surface.ports_kick",
"surface.read_text",
@@ -6595,6 +6598,86 @@ class TerminalController {
return result
}
private func v2SurfaceReportPwd(params: [String: Any]) -> V2CallResult {
guard let workspaceId = v2UUID(params, "workspace_id") else {
return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil)
}
let requestedSurfaceId = v2UUID(params, "surface_id")
if v2HasNonNullParam(params, "surface_id"), requestedSurfaceId == nil {
return .err(code: "invalid_params", message: "Missing or invalid surface_id", data: nil)
}
let rawDirectory = v2RawString(params, "directory")
?? v2RawString(params, "path")
?? v2RawString(params, "pwd")
guard let directory = rawDirectory, !directory.isEmpty else {
return .err(code: "invalid_params", message: "Missing directory", data: nil)
}
if let requestedSurfaceId {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard let tabManager = AppDelegate.shared?.tabManagerFor(tabId: workspaceId) ?? self.tabManager,
let tab = tabManager.tabs.first(where: { $0.id == workspaceId }) else {
return
}
let validSurfaceIds = Set(tab.panels.keys)
tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds)
guard validSurfaceIds.contains(requestedSurfaceId) else { return }
tabManager.updateSurfaceDirectory(
tabId: workspaceId,
surfaceId: requestedSurfaceId,
directory: directory,
preserveExactDirectory: true
)
}
return .ok([
"workspace_id": workspaceId.uuidString,
"workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId),
"surface_id": requestedSurfaceId.uuidString,
"surface_ref": v2Ref(kind: .surface, uuid: requestedSurfaceId),
"directory": directory,
"accepted": true,
"queued": true,
])
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard let tab = self.tabForSidebarMutation(id: workspaceId),
let tabManager = AppDelegate.shared?.tabManagerFor(tabId: workspaceId) ?? self.tabManager else {
return
}
let validSurfaceIds = Set(tab.panels.keys)
tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds)
let surfaceId = self.resolveReportedSurfaceId(
in: tab,
requestedSurfaceId: requestedSurfaceId,
validSurfaceIds: validSurfaceIds
)
guard let surfaceId, validSurfaceIds.contains(surfaceId) else {
return
}
tabManager.updateSurfaceDirectory(
tabId: workspaceId,
surfaceId: surfaceId,
directory: directory,
preserveExactDirectory: true
)
}
return .ok([
"workspace_id": workspaceId.uuidString,
"workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId),
"surface_id": NSNull(),
"surface_ref": NSNull(),
"directory": directory,
"accepted": true,
"queued": true,
])
}
private func v2SurfaceReportShellState(params: [String: Any]) -> V2CallResult {
guard let workspaceId = v2UUID(params, "workspace_id") else {
return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil)
+158 -41
View File
@@ -12202,32 +12202,48 @@ final class Workspace: Identifiable, ObservableObject {
}
@discardableResult
func updatePanelDirectory(panelId: UUID, directory: String) -> Bool {
updatePanelDirectory(panelId: panelId, directory: directory, source: .liveReport)
func updatePanelDirectory(
panelId: UUID,
directory: String,
preserveExactDirectory: Bool = false
) -> Bool {
updatePanelDirectory(
panelId: panelId,
directory: directory,
preserveExactDirectory: preserveExactDirectory,
source: .liveReport
)
}
@discardableResult
private func updatePanelDirectory(
panelId: UUID,
directory: String,
preserveExactDirectory: Bool = false,
source: PanelDirectoryUpdateSource
) -> Bool {
let trimmed = directory.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
let resolvedDirectory: String
if preserveExactDirectory {
resolvedDirectory = directory
} else {
resolvedDirectory = directory.trimmingCharacters(in: .whitespacesAndNewlines)
}
guard !resolvedDirectory.isEmpty else { return false }
if source == .liveReport,
shouldIgnoreRestoredGuardedDirectoryReport(panelId: panelId, reportedDirectory: trimmed) {
!preserveExactDirectory,
shouldIgnoreRestoredGuardedDirectoryReport(panelId: panelId, reportedDirectory: resolvedDirectory) {
return false
}
if panelDirectories[panelId] != trimmed {
panelDirectories[panelId] = trimmed
if panelDirectories[panelId] != resolvedDirectory {
panelDirectories[panelId] = resolvedDirectory
}
// Update current directory if this is the focused panel
if panelId == focusedPanelId {
if surfaceTabBarDirectory != trimmed {
surfaceTabBarDirectory = trimmed
if surfaceTabBarDirectory != resolvedDirectory {
surfaceTabBarDirectory = resolvedDirectory
}
if currentDirectory != trimmed {
currentDirectory = trimmed
if currentDirectory != resolvedDirectory {
currentDirectory = resolvedDirectory
}
}
return true
@@ -13415,6 +13431,77 @@ final class Workspace: Identifiable, ObservableObject {
return environment
}
private func normalizedTerminalWorkingDirectory(
_ workingDirectory: String?,
preserveExact: Bool = false
) -> String? {
guard let workingDirectory else { return nil }
if preserveExact {
return workingDirectory.isEmpty ? nil : workingDirectory
}
let trimmed = workingDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private func terminalWorkingDirectoryCandidate(
for panelId: UUID?,
preserveExact: Bool
) -> String? {
guard let panelId else { return nil }
if let panelDirectory = normalizedTerminalWorkingDirectory(
panelDirectories[panelId],
preserveExact: preserveExact
) {
return panelDirectory
}
return normalizedTerminalWorkingDirectory(
terminalPanel(for: panelId)?.requestedWorkingDirectory,
preserveExact: preserveExact
)
}
private func resolvedTerminalStartupWorkingDirectory(
explicitWorkingDirectory: String?,
sourcePanelId: UUID? = nil,
targetPaneId: PaneID? = nil,
preserveExact: Bool = false
) -> String? {
if let explicitWorkingDirectory = normalizedTerminalWorkingDirectory(
explicitWorkingDirectory,
preserveExact: preserveExact
) {
return explicitWorkingDirectory
}
var candidatePanelIds: [UUID] = []
var seenPanelIds: Set<UUID> = []
func appendCandidate(_ panelId: UUID?) {
guard let panelId, seenPanelIds.insert(panelId).inserted else { return }
candidatePanelIds.append(panelId)
}
appendCandidate(sourcePanelId)
if let targetPaneId {
if let selectedSurfaceId = bonsplitController.selectedTab(inPane: targetPaneId)?.id {
appendCandidate(panelIdFromSurfaceId(selectedSurfaceId))
}
for tab in bonsplitController.tabs(inPane: targetPaneId) {
appendCandidate(panelIdFromSurfaceId(tab.id))
}
}
for panelId in candidatePanelIds {
if let candidate = terminalWorkingDirectoryCandidate(
for: panelId,
preserveExact: preserveExact
) {
return candidate
}
}
return normalizedTerminalWorkingDirectory(currentDirectory, preserveExact: preserveExact)
}
private func normalizedRemotePTYSessionID(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
@@ -14357,7 +14444,7 @@ final class Workspace: Identifiable, ObservableObject {
let remoteTerminalStartupCommand = remoteTerminalStartupCommand()
let startupCommand = explicitInitialCommand ?? remoteTerminalStartupCommand
let remoteStartupCommandForEnvironment = explicitInitialCommand == nil ? remoteTerminalStartupCommand : nil
let effectiveStartupEnvironment = terminalStartupEnvironment(
var effectiveStartupEnvironment = terminalStartupEnvironment(
base: startupEnvironment,
remoteStartupCommand: remoteStartupCommandForEnvironment
)
@@ -14382,36 +14469,29 @@ final class Workspace: Identifiable, ObservableObject {
// Inherit working directory: prefer the source panel's reported cwd,
// then its requested startup cwd if shell integration has not reported
// back yet, and finally fall back to the workspace's current directory.
let splitWorkingDirectory: String? = {
if let workingDirectory = workingDirectory?.trimmingCharacters(in: .whitespacesAndNewlines),
!workingDirectory.isEmpty {
return workingDirectory
}
if let panelDirectory = panelDirectories[panelId]?.trimmingCharacters(in: .whitespacesAndNewlines),
!panelDirectory.isEmpty {
return panelDirectory
}
if let requestedWorkingDirectory = terminalPanel(for: panelId)?
.requestedWorkingDirectory?
.trimmingCharacters(in: .whitespacesAndNewlines),
!requestedWorkingDirectory.isEmpty {
return requestedWorkingDirectory
}
let workspaceDirectory = currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
return workspaceDirectory.isEmpty ? nil : workspaceDirectory
}()
let splitWorkingDirectory = resolvedTerminalStartupWorkingDirectory(
explicitWorkingDirectory: workingDirectory,
sourcePanelId: panelId,
preserveExact: remoteStartupCommandForEnvironment != nil
)
#if DEBUG
cmuxDebugLog(
"split.cwd panelId=\(panelId.uuidString.prefix(5)) panelDir=\(panelDirectories[panelId] ?? "nil") requestedDir=\(terminalPanel(for: panelId)?.requestedWorkingDirectory ?? "nil") currentDir=\(currentDirectory) resolved=\(splitWorkingDirectory ?? "nil")"
)
#endif
let usesWorkspaceRemoteStartup = remoteStartupCommandForEnvironment != nil
let remoteInitialWorkingDirectory = usesWorkspaceRemoteStartup ? splitWorkingDirectory : nil
let localWorkingDirectory = usesWorkspaceRemoteStartup ? nil : splitWorkingDirectory
if let remoteInitialWorkingDirectory {
effectiveStartupEnvironment["CMUX_REMOTE_INITIAL_CWD"] = remoteInitialWorkingDirectory
}
// Create the new terminal panel.
let newPanel = TerminalPanel(
workspaceId: id,
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: inheritedConfig,
workingDirectory: splitWorkingDirectory,
workingDirectory: localWorkingDirectory,
portOrdinal: portOrdinal,
initialCommand: startupCommand,
tmuxStartCommand: tmuxStartCommand,
@@ -14542,7 +14622,7 @@ final class Workspace: Identifiable, ObservableObject {
let remoteTerminalStartupCommand = suppressWorkspaceRemoteStartupCommand ? nil : remoteTerminalStartupCommand()
let startupCommand = explicitInitialCommand ?? remoteTerminalStartupCommand
let remoteStartupCommandForEnvironment = explicitInitialCommand == nil ? remoteTerminalStartupCommand : nil
let effectiveStartupEnvironment = terminalStartupEnvironment(
var effectiveStartupEnvironment = terminalStartupEnvironment(
base: startupEnvironment,
remoteStartupCommand: remoteStartupCommandForEnvironment
)
@@ -14554,13 +14634,27 @@ final class Workspace: Identifiable, ObservableObject {
template.waitAfterCommand = true
inheritedConfig = template
}
let requestedWorkingDirectory = normalizedTerminalWorkingDirectory(workingDirectory)
let localWorkingDirectory: String?
if remoteStartupCommandForEnvironment != nil {
localWorkingDirectory = nil
if let remoteInitialWorkingDirectory = resolvedTerminalStartupWorkingDirectory(
explicitWorkingDirectory: workingDirectory,
targetPaneId: paneId,
preserveExact: true
) {
effectiveStartupEnvironment["CMUX_REMOTE_INITIAL_CWD"] = remoteInitialWorkingDirectory
}
} else {
localWorkingDirectory = requestedWorkingDirectory
}
// Create new terminal panel
let newPanel = TerminalPanel(
workspaceId: id,
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: inheritedConfig,
workingDirectory: workingDirectory,
workingDirectory: localWorkingDirectory,
portOrdinal: portOrdinal,
initialCommand: startupCommand,
tmuxStartCommand: tmuxStartCommand,
@@ -17807,21 +17901,30 @@ final class Workspace: Identifiable, ObservableObject {
var inheritedConfig = inheritedTerminalConfig(inPane: paneId)
let requestedRemoteStartupCommand = remoteStartupCommand?.trimmingCharacters(in: .whitespacesAndNewlines)
let startupCommand = requestedRemoteStartupCommand?.isEmpty == false ? requestedRemoteStartupCommand : nil
let effectiveStartupEnvironment = terminalStartupEnvironment(
let remoteInitialWorkingDirectory = startupCommand == nil ? nil : resolvedTerminalStartupWorkingDirectory(
explicitWorkingDirectory: workingDirectory,
targetPaneId: paneId,
preserveExact: true
)
var effectiveStartupEnvironment = terminalStartupEnvironment(
base: [:],
remoteStartupCommand: startupCommand
)
if let remoteInitialWorkingDirectory {
effectiveStartupEnvironment["CMUX_REMOTE_INITIAL_CWD"] = remoteInitialWorkingDirectory
}
if startupCommand != nil {
var template = inheritedConfig ?? CmuxSurfaceConfigTemplate()
template.waitAfterCommand = true
inheritedConfig = template
}
let terminalWorkingDirectory = startupCommand == nil ? workingDirectory : nil
let newPanel = TerminalPanel(
workspaceId: id,
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: inheritedConfig,
workingDirectory: workingDirectory,
workingDirectory: terminalWorkingDirectory,
portOrdinal: portOrdinal,
initialCommand: startupCommand,
initialInput: initialInput,
@@ -17935,14 +18038,14 @@ final class Workspace: Identifiable, ObservableObject {
targetPane: paneId,
orientation: direction.orientation,
insertFirst: direction.insertFirst,
workingDirectory: remoteStartupCommand == nil ? workingDirectory : nil,
workingDirectory: workingDirectory,
initialInput: startupInput,
remoteStartupCommand: remoteStartupCommand
)
if let forkedPanel,
remoteStartupCommand != nil,
let workingDirectory {
updatePanelDirectory(panelId: forkedPanel.id, directory: workingDirectory)
updatePanelDirectory(panelId: forkedPanel.id, directory: workingDirectory, preserveExactDirectory: true)
}
if forkedPanel == nil, let zoomedPaneId {
_ = bonsplitController.togglePaneZoom(inPane: zoomedPaneId)
@@ -17959,7 +18062,7 @@ final class Workspace: Identifiable, ObservableObject {
panelDirectories[panelId],
terminalPanel(for: panelId)?.requestedWorkingDirectory,
currentDirectory
])
], preserveExact: isRemoteTerminalSurface(panelId))
}
/// Synchronous availability check used by the tab right-click context menu to decide
@@ -18031,13 +18134,13 @@ final class Workspace: Identifiable, ObservableObject {
let forkedPanel = newTerminalSurface(
inPane: paneId,
focus: true,
workingDirectory: remoteStartupCommand == nil ? workingDirectory : nil,
workingDirectory: workingDirectory,
initialInput: startupInput
)
if let forkedPanel {
_ = reorderSurface(panelId: forkedPanel.id, toIndex: targetIndex)
if remoteStartupCommand != nil, let workingDirectory {
updatePanelDirectory(panelId: forkedPanel.id, directory: workingDirectory)
updatePanelDirectory(panelId: forkedPanel.id, directory: workingDirectory, preserveExactDirectory: true)
}
} else if let zoomedPaneId {
_ = bonsplitController.togglePaneZoom(inPane: zoomedPaneId)
@@ -18063,7 +18166,17 @@ final class Workspace: Identifiable, ObservableObject {
}
private static func firstNonEmptyPath(_ candidates: [String?]) -> String? {
firstNonEmptyPath(candidates, preserveExact: false)
}
private static func firstNonEmptyPath(_ candidates: [String?], preserveExact: Bool) -> String? {
for candidate in candidates {
if preserveExact {
if let candidate, !candidate.isEmpty {
return candidate
}
continue
}
let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines)
if let trimmed, !trimmed.isEmpty {
return trimmed
@@ -19532,7 +19645,11 @@ extension Workspace: BonsplitDelegate {
if let workingDirectory = launch.workingDirectory,
launch.terminalWorkingDirectory == nil,
let forkPanelId = forkWorkspace.focusedPanelId {
forkWorkspace.updatePanelDirectory(panelId: forkPanelId, directory: workingDirectory)
forkWorkspace.updatePanelDirectory(
panelId: forkPanelId,
directory: workingDirectory,
preserveExactDirectory: launch.remoteConfiguration != nil
)
}
return true
}
+365
View File
@@ -5315,6 +5315,7 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
printf '%s\\n' '{"ok":true,"result":{"accepted":true,"queued":true}}'
exit 0
"""
)
@@ -5343,6 +5344,189 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
)
}
func testShellIntegrationRelayPromptReportsPWDInZsh() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-relay-report-pwd-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
exit 0
"""
)
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_precmd
repeat 20; do
command grep -q "surface.report_pwd" "\(logPath.path)" && break
sleep 0.05
done
repeat 20; do
[[ -s "${_CMUX_PWD_RELAY_ACK_FILE:-}" ]] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_precmd
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
XCTAssertTrue(
output.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","surface_id":"22222222-2222-2222-2222-222222222222","directory":"\#(remoteDirectory.path)"}"#),
output
)
let reportCount = output.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 1, output)
XCTAssertTrue(output.contains("LAST=\(remoteDirectory.path)\n"), output)
}
func testShellIntegrationRelayPromptRetriesPWDWhenRelayFailsInZsh() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-relay-report-pwd-retry-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
exit 1
"""
)
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_precmd
repeat 20; do
_cmux_count="$(command grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[[ "${_cmux_count:-0}" -ge 1 ]] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_precmd
repeat 20; do
_cmux_count="$(command grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[[ "${_cmux_count:-0}" -ge 2 ]] && break
sleep 0.05
done
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
let reportCount = output.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 2, output)
XCTAssertTrue(output.contains("LAST=\n"), output)
}
func testShellIntegrationRelayPromptDoesNotAckPendingPWDInZsh() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-relay-report-pwd-pending-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
printf '%s\\n' '{"ok":true,"result":{"pending":true}}'
exit 0
"""
)
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_precmd
repeat 20; do
_cmux_count="$(command grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[[ "${_cmux_count:-0}" -ge 1 ]] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_precmd
repeat 20; do
_cmux_count="$(command grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[[ "${_cmux_count:-0}" -ge 2 ]] && break
sleep 0.05
done
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
let reportCount = output.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 2, output)
XCTAssertTrue(output.contains("LAST=\n"), output)
}
func testShellIntegrationRelayPortsKickOmitsSurfaceIDUntilAvailableInZsh() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -5358,6 +5542,7 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
printf '%s\\n' '{"ok":true,"result":{"accepted":true,"queued":true}}'
exit 0
"""
)
@@ -5480,6 +5665,186 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
)
}
func testShellIntegrationRelayPromptReportsPWDInBash() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-bash-relay-report-pwd-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
exit 0
"""
)
let result = try runInteractiveBash(
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_prompt_command
for _cmux_i in $(seq 1 20); do
grep -q "surface.report_pwd" "\(logPath.path)" && break
sleep 0.05
done
for _cmux_i in $(seq 1 20); do
[ -s "${_CMUX_PWD_RELAY_ACK_FILE:-}" ] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_prompt_command
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
XCTAssertTrue(
result.stdout.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","surface_id":"22222222-2222-2222-2222-222222222222","directory":"\#(remoteDirectory.path)"}"#),
result.stdout
)
let reportCount = result.stdout.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 1, result.stdout)
XCTAssertTrue(result.stdout.contains("LAST=\(remoteDirectory.path)\n"), result.stdout)
}
func testShellIntegrationRelayPromptRetriesPWDWhenRelayFailsInBash() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-bash-relay-report-pwd-retry-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
exit 1
"""
)
let result = try runInteractiveBash(
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_prompt_command
for _cmux_i in $(seq 1 20); do
_cmux_count="$(grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[ "${_cmux_count:-0}" -ge 1 ] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_prompt_command
for _cmux_i in $(seq 1 20); do
_cmux_count="$(grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[ "${_cmux_count:-0}" -ge 2 ] && break
sleep 0.05
done
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
let reportCount = result.stdout.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 2, result.stdout)
XCTAssertTrue(result.stdout.contains("LAST=\n"), result.stdout)
}
func testShellIntegrationRelayPromptDoesNotAckPendingPWDInBash() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-bash-relay-report-pwd-pending-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let remoteDirectory = root.appendingPathComponent("remote project", isDirectory: true)
let logPath = root.appendingPathComponent("relay.log", isDirectory: false)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableScript(
at: binDir.appendingPathComponent("cmux", isDirectory: false),
contents: """
#!/bin/sh
printf '%s\\n' "$*" >> "\(logPath.path)"
printf '%s\\n' '{"ok":true,"result":{"pending":true}}'
exit 0
"""
)
let result = try runInteractiveBash(
cmuxLoadShellIntegration: true,
command: """
: > "\(logPath.path)"
cd "\(remoteDirectory.path)"
_CMUX_TTY_REPORTED=1
_CMUX_PORTS_LAST_RUN="$(_cmux_now)"
_CMUX_PWD_LAST_PWD=
_cmux_prompt_command
for _cmux_i in $(seq 1 20); do
_cmux_count="$(grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[ "${_cmux_count:-0}" -ge 1 ] && break
sleep 0.05
done
_CMUX_PWD_RELAY_PENDING_STARTED_AT=0
_cmux_prompt_command
for _cmux_i in $(seq 1 20); do
_cmux_count="$(grep -c "surface.report_pwd" "\(logPath.path)" 2>/dev/null || true)"
[ "${_cmux_count:-0}" -ge 2 ] && break
sleep 0.05
done
printf 'LAST=%s\\n' "$_CMUX_PWD_LAST_PWD"
cat "\(logPath.path)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"CMUX_SOCKET_PATH": "127.0.0.1:64011",
"CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222",
"CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222",
]
)
let reportCount = result.stdout.components(separatedBy: "rpc surface.report_pwd").count - 1
XCTAssertEqual(reportCount, 2, result.stdout)
XCTAssertTrue(result.stdout.contains("LAST=\n"), result.stdout)
}
func testShellIntegrationRelayPreexecWorksBeforeSurfaceIDExistsInBash() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
+77
View File
@@ -73,6 +73,7 @@ private func restoreUserDefaultForTabManagerTests(_ value: Any?, key: String) {
private actor BlockingWorkspaceGitMetadataReader: WorkspaceGitMetadataReading {
private let metadata: GitWorkspaceMetadata
private var directories: [String] = []
private var callCount = 0
private var maxActiveCallCount = 0
private var activeCallCount = 0
@@ -84,6 +85,7 @@ private actor BlockingWorkspaceGitMetadataReader: WorkspaceGitMetadataReading {
}
func workspaceMetadata(for directory: String) async -> GitWorkspaceMetadata {
directories.append(directory)
callCount += 1
activeCallCount += 1
maxActiveCallCount = max(maxActiveCallCount, activeCallCount)
@@ -114,6 +116,10 @@ private actor BlockingWorkspaceGitMetadataReader: WorkspaceGitMetadataReading {
callCount
}
var observedDirectories: [String] {
directories
}
var observedMaxActiveCallCount: Int {
maxActiveCallCount
}
@@ -1030,6 +1036,77 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
XCTAssertEqual(manager.activeWorkspaceGitProbePanelIdsForTesting(workspaceId: workspace.id), Set<UUID>())
}
func testRemoteDirectoryChangeGitProbePreservesExactReportedDirectory() async throws {
let defaults = UserDefaults.standard
let previousWatchGitStatus = defaults.object(forKey: SidebarWorkspaceDetailDefaults.watchGitStatusKey)
defaults.set(false, forKey: SidebarWorkspaceDetailDefaults.watchGitStatusKey)
defer {
restoreUserDefaultForTabManagerTests(
previousWatchGitStatus,
key: SidebarWorkspaceDetailDefaults.watchGitStatusKey
)
}
let reader = BlockingWorkspaceGitMetadataReader(
metadata: GitWorkspaceMetadata(
isRepository: false,
branch: nil,
isDirty: false,
indexSignature: nil,
indexContentSignature: nil,
headSignature: nil
)
)
defer {
Task {
await reader.releaseAll()
}
}
let manager = TabManager(workspaceGitMetadataReader: reader)
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused panel")
return
}
workspace.configureRemoteConnection(
WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
port: nil,
identityFile: nil,
sshOptions: [],
localProxyPort: nil,
relayPort: 64017,
relayID: String(repeating: "a", count: 16),
relayToken: String(repeating: "b", count: 64),
localSocketPath: "/tmp/cmux-debug-test.sock",
terminalStartupCommand: "ssh cmux-macmini"
),
autoConnect: false
)
let exactDirectory = "/srv/cmux/repo-\(UUID().uuidString) "
defaults.set(true, forKey: SidebarWorkspaceDetailDefaults.watchGitStatusKey)
manager.updateSurfaceDirectory(
tabId: workspace.id,
surfaceId: panelId,
directory: exactDirectory,
preserveExactDirectory: true
)
let readStarted = expectation(description: "remote exact cwd git snapshot read started")
Task {
await reader.waitForCallCount(1)
readStarted.fulfill()
}
await fulfillment(of: [readStarted], timeout: 1.0)
let observedDirectories = await reader.observedDirectories
XCTAssertEqual(observedDirectories, [exactDirectory])
await reader.releaseAll()
}
// testResolvedCommandPathFallsBackOutsideAppPATH moved to
// CmuxProcessTests.resolvesCommandViaFallbackDirectoryOutsidePath when the
// command runner was extracted into the CmuxProcess package.
@@ -202,6 +202,62 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
XCTAssertEqual(payload["has_ssh_options"] as? Bool, true)
}
func testV2SurfaceReportPwdUpdatesRemoteWorkspaceDirectory() throws {
let tabManager = TabManager()
TerminalController.shared.setActiveTabManager(tabManager)
defer { TerminalController.shared.setActiveTabManager(nil) }
let workspace = tabManager.addWorkspace(
workingDirectory: "/Users/local/project",
select: true,
eagerLoadTerminal: false
)
workspace.configureRemoteConnection(
.init(
destination: "example.com",
port: nil,
identityFile: nil,
sshOptions: [],
localProxyPort: nil,
relayPort: 64011,
relayID: "relay-id",
relayToken: "relay-token",
localSocketPath: "/tmp/cmux-test.sock",
terminalStartupCommand: "ssh example.com"
),
autoConnect: false
)
let surfaceId = try XCTUnwrap(workspace.focusedPanelId)
let directory = "/home/dev/project with trailing space "
let request: [String: Any] = [
"id": "report-pwd",
"method": "surface.report_pwd",
"params": [
"workspace_id": workspace.id.uuidString,
"surface_id": surfaceId.uuidString,
"directory": directory,
],
]
let requestData = try JSONSerialization.data(withJSONObject: request, options: [])
let requestLine = try XCTUnwrap(String(data: requestData, encoding: .utf8))
let responseLine = TerminalController.shared.handleSocketLine(requestLine)
let responseData = try XCTUnwrap(responseLine.data(using: .utf8))
let response = try XCTUnwrap(JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any])
XCTAssertEqual(response["ok"] as? Bool, true)
let result = try XCTUnwrap(response["result"] as? [String: Any])
XCTAssertEqual(result["accepted"] as? Bool, true)
XCTAssertEqual(result["queued"] as? Bool, true)
XCTAssertNil(result["pending"])
let deadline = Date().addingTimeInterval(1.0)
while workspace.currentDirectory != directory && Date() < deadline {
RunLoop.current.run(until: Date().addingTimeInterval(0.01))
}
XCTAssertEqual(workspace.panelDirectories[surfaceId], directory)
XCTAssertEqual(workspace.currentDirectory, directory)
}
func testRemoteConfigureRejectsInvalidPersistentDaemonSlot() throws {
let response = try handleV2Request(
method: "workspace.remote.configure",
@@ -257,6 +257,73 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(cmuxBinEntries.count, 1, path)
}
func testGeneratedBashBootstrapChangesToInitialRemoteWorkingDirectory() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-initial-remote-cwd-\(UUID().uuidString)")
let home = root.appendingPathComponent("home")
let bin = root.appendingPathComponent("bin")
let initialWorkingDirectory = root.appendingPathComponent("remote-project")
let capturedPWD = root.appendingPathComponent("pwd.txt")
try fileManager.createDirectory(at: home, withIntermediateDirectories: true)
try fileManager.createDirectory(at: bin, withIntermediateDirectories: true)
try fileManager.createDirectory(at: initialWorkingDirectory, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
try writeExecutableShellFile(
at: bin.appendingPathComponent("bash"),
body: """
#!/bin/sh
rcfile=
while [ "$#" -gt 0 ]; do
case "$1" in
--rcfile)
shift
rcfile="${1:-}"
;;
esac
shift || true
done
if [ -n "$rcfile" ]; then
. "$rcfile"
fi
printf '%s\\n' "$PWD" > "$CMUX_CAPTURE_PWD"
"""
)
let encodedWorkingDirectory = Data(initialWorkingDirectory.path.utf8).base64EncodedString()
let script = RemoteInteractiveShellBootstrapBuilder.script(
remoteRelayPort: 0,
shellFeatures: ""
)
.replacingOccurrences(
of: "__CMUX_REMOTE_INITIAL_CWD_B64__",
with: encodedWorkingDirectory
)
let result = runProcess(
executablePath: "/usr/bin/env",
arguments: [
"HOME=\(home.path)",
"SHELL=\(bin.appendingPathComponent("bash").path)",
"PATH=\(bin.path):/usr/bin:/bin",
"TERM=xterm-256color",
"USER=\(NSUserName())",
"CMUX_CAPTURE_PWD=\(capturedPWD.path)",
"/bin/sh",
"-c",
script,
],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
let captured = try String(contentsOf: capturedPWD, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(captured, initialWorkingDirectory.path)
}
func testRemoteRelayMetadataCleanupScriptRemovesMatchingSocketAddr() {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-relay-cleanup-\(UUID().uuidString)")
@@ -2370,6 +2437,52 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, 0)
}
@MainActor
func testRemoteTerminalSurfaceInheritsSelectedPanelWorkingDirectory() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
port: nil,
identityFile: nil,
sshOptions: [],
localProxyPort: nil,
relayPort: 64016,
relayID: String(repeating: "a", count: 16),
relayToken: String(repeating: "b", count: 64),
localSocketPath: "/tmp/cmux-debug-test.sock",
terminalStartupCommand: "ssh-pty-attach",
preserveAfterTerminalExit: true
)
workspace.configureRemoteConnection(config, autoConnect: false)
let sourcePanelID = try XCTUnwrap(workspace.focusedTerminalPanel?.id)
let paneID = try XCTUnwrap(workspace.paneId(forPanelId: sourcePanelID))
let selectedPanelDirectory = "/srv/cmux/selected-\(UUID().uuidString) "
let staleWorkspaceDirectory = "/srv/cmux/stale-\(UUID().uuidString)"
workspace.updatePanelDirectory(
panelId: sourcePanelID,
directory: selectedPanelDirectory,
preserveExactDirectory: true
)
workspace.currentDirectory = staleWorkspaceDirectory
let panel = try XCTUnwrap(
workspace.newTerminalSurface(
inPane: paneID,
focus: false
)
)
XCTAssertNil(
panel.requestedWorkingDirectory,
"Remote workspace startup must not pass the remote cwd as a local Ghostty working directory"
)
XCTAssertEqual(
panel.surface.startupEnvironmentValue("CMUX_REMOTE_INITIAL_CWD"),
selectedPanelDirectory
)
}
@MainActor
func testRemoteDisconnectClearsExplicitRemotePTYSessionIDBeforeReseed() throws {
let workspace = Workspace()
+49 -3
View File
@@ -5749,12 +5749,12 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
let snapshot = SessionRestorableAgentSnapshot(
kind: .codex,
sessionId: "019dad34-d218-7943-b81a-eddac5c87951",
workingDirectory: "/Users/cmux/project",
workingDirectory: "/Users/cmux/project ",
launchCommand: AgentLaunchCommandSnapshot(
launcher: "codex",
executablePath: "/Users/example/.bun/bin/codex",
arguments: ["/Users/example/.bun/bin/codex"],
workingDirectory: "/Users/cmux/project",
workingDirectory: "/Users/cmux/project ",
environment: nil,
capturedAt: 123,
source: "process"
@@ -5771,7 +5771,8 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
XCTAssertEqual(forkPanel.surface.debugInitialCommand(), "ssh cmux-macmini")
XCTAssertNil(forkPanel.requestedWorkingDirectory)
XCTAssertEqual(workspace.panelDirectories[forkPanel.id], "/Users/cmux/project")
XCTAssertEqual(forkPanel.surface.startupEnvironmentValue("CMUX_REMOTE_INITIAL_CWD"), "/Users/cmux/project ")
XCTAssertEqual(workspace.panelDirectories[forkPanel.id], "/Users/cmux/project ")
XCTAssertEqual(forkPanel.surface.initialInput, snapshot.forkCommand.map { $0 + "\n" })
XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, initialRemoteSessionCount + 1)
}
@@ -5820,6 +5821,10 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
XCTAssertEqual(forkPanel.surface.debugInitialCommand(), "ssh cmux-macmini")
XCTAssertNil(forkPanel.requestedWorkingDirectory)
XCTAssertEqual(
forkPanel.surface.startupEnvironmentValue("CMUX_REMOTE_INITIAL_CWD"),
"/Users/cmux/fallback repo"
)
XCTAssertEqual(workspace.panelDirectories[forkPanel.id], "/Users/cmux/fallback repo")
XCTAssertEqual(
forkPanel.surface.initialInput,
@@ -6755,6 +6760,47 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
)
}
func testForkAgentConversationToNewTabInRemoteWorkspaceUsesRemoteInitialCWD() throws {
let workspace = Workspace()
workspace.configureRemoteConnection(
WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
port: nil,
identityFile: nil,
sshOptions: [],
localProxyPort: nil,
relayPort: 64000,
relayID: "relay-new-tab-fork",
relayToken: String(repeating: "a", count: 64),
localSocketPath: "/tmp/cmux-new-tab-fork-remote.sock",
terminalStartupCommand: "ssh cmux-macmini"
),
autoConnect: false
)
let initialRemoteSessionCount = workspace.activeRemoteTerminalSessionCount
let sourcePanelId = try XCTUnwrap(workspace.focusedPanelId)
let sourcePaneId = try XCTUnwrap(workspace.paneId(forPanelId: sourcePanelId))
let anchorTabId = try XCTUnwrap(workspace.surfaceIdFromPanelId(sourcePanelId))
let remoteWorkingDirectory = "/Users/cmux/new tab project "
let snapshot = makeForkableCodexSnapshot(workingDirectory: remoteWorkingDirectory)
let forkPanel = try XCTUnwrap(
workspace.forkAgentConversationToNewTab(
fromPanelId: sourcePanelId,
snapshot: snapshot,
anchorTabId: anchorTabId,
paneId: sourcePaneId
)
)
XCTAssertEqual(forkPanel.surface.debugInitialCommand(), "ssh cmux-macmini")
XCTAssertNil(forkPanel.requestedWorkingDirectory)
XCTAssertEqual(forkPanel.surface.startupEnvironmentValue("CMUX_REMOTE_INITIAL_CWD"), remoteWorkingDirectory)
XCTAssertEqual(workspace.panelDirectories[forkPanel.id], remoteWorkingDirectory)
XCTAssertEqual(forkPanel.surface.initialInput, snapshot.forkCommand.map { $0 + "\n" })
XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, initialRemoteSessionCount + 1)
}
func testForkAgentConversationToNewTabPlacesForkImmediatelyRightOfAnchor() throws {
let workspace = Workspace()
let sourcePanelId = try XCTUnwrap(workspace.focusedPanelId)