Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67a334437 | ||
|
|
8b6a59a00b | ||
|
|
831dd44fcc | ||
|
|
93806b7eca | ||
|
|
ea60a6eadc | ||
|
|
d5a2dda37d | ||
|
|
14a4ef1b82 | ||
|
|
8f5e487be4 | ||
|
|
6097667666 | ||
|
|
a976bd919a | ||
|
|
ba5cc9a915 | ||
|
|
ef935bff3f | ||
|
|
7817c4513c | ||
|
|
5044f84aba | ||
|
|
4d0d50b3e0 | ||
|
|
0d76554751 |
+20
-2
@@ -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'",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user