Merge branch 'main' into issue-151-ssh-remote-port-proxying

# Conflicts:
#	CLI/cmux.swift
#	Sources/ContentView.swift
#	Sources/GhosttyTerminalView.swift
#	Sources/Panels/BrowserPanel.swift
#	Sources/Panels/BrowserPanelView.swift
#	Sources/TabManager.swift
#	Sources/TerminalController.swift
#	Sources/Workspace.swift
#	Sources/WorkspaceContentView.swift
#	ghostty
This commit is contained in:
Lawrence Chen 2026-03-09 18:36:59 -07:00
commit bdebc8ecc9
205 changed files with 107859 additions and 6333 deletions

View file

@ -13,7 +13,9 @@
# - CMUX_ZSH_ZDOTDIR (set by cmux when it overwrote a user-provided ZDOTDIR)
# - unset (zsh treats unset ZDOTDIR as $HOME)
builtin typeset _cmux_had_ghostty_zdotdir=0
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
_cmux_had_ghostty_zdotdir=1
builtin export ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
builtin unset GHOSTTY_ZSH_ZDOTDIR
elif [[ -n "${CMUX_ZSH_ZDOTDIR+X}" ]]; then
@ -31,7 +33,9 @@ fi
if [[ -o interactive ]]; then
# We overwrote GhosttyKit's injected ZDOTDIR, so manually load Ghostty's
# zsh integration if available.
if [[ -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
# Guard on GHOSTTY_ZSH_ZDOTDIR being set by Ghostty. When users configure
# shell-integration=none, Ghostty does not set this and we must skip.
if [[ "$_cmux_had_ghostty_zdotdir" == "1" && -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
builtin typeset _cmux_ghostty="$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
[[ -r "$_cmux_ghostty" ]] && builtin source -- "$_cmux_ghostty"
fi
@ -43,5 +47,5 @@ fi
fi
fi
builtin unset _cmux_file _cmux_ghostty _cmux_integ
builtin unset _cmux_file _cmux_ghostty _cmux_integ _cmux_had_ghostty_zdotdir
}

View file

@ -41,6 +41,9 @@ _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:-}"
_CMUX_GIT_JOB_STARTED_AT="${_CMUX_GIT_JOB_STARTED_AT:-0}"
_CMUX_GIT_HEAD_LAST_PWD="${_CMUX_GIT_HEAD_LAST_PWD:-}"
_CMUX_GIT_HEAD_PATH="${_CMUX_GIT_HEAD_PATH:-}"
_CMUX_GIT_HEAD_SIGNATURE="${_CMUX_GIT_HEAD_SIGNATURE:-}"
_CMUX_PR_LAST_PWD="${_CMUX_PR_LAST_PWD:-}"
_CMUX_PR_LAST_RUN="${_CMUX_PR_LAST_RUN:-0}"
_CMUX_PR_JOB_PID="${_CMUX_PR_JOB_PID:-}"
@ -51,6 +54,41 @@ _CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
_CMUX_TTY_REPORTED="${_CMUX_TTY_REPORTED:-0}"
_cmux_git_resolve_head_path() {
# Resolve the HEAD file path without invoking git (fast; works for worktrees).
local dir="$PWD"
while :; do
if [[ -d "$dir/.git" ]]; then
printf '%s\n' "$dir/.git/HEAD"
return 0
fi
if [[ -f "$dir/.git" ]]; then
local line gitdir
IFS= read -r line < "$dir/.git" || line=""
if [[ "$line" == gitdir:* ]]; then
gitdir="${line#gitdir:}"
gitdir="${gitdir## }"
gitdir="${gitdir%% }"
[[ -n "$gitdir" ]] || return 1
[[ "$gitdir" != /* ]] && gitdir="$dir/$gitdir"
printf '%s\n' "$gitdir/HEAD"
return 0
fi
fi
[[ "$dir" == "/" || -z "$dir" ]] && break
dir="$(dirname "$dir")"
done
return 1
}
_cmux_git_head_signature() {
local head_path="$1"
[[ -n "$head_path" && -r "$head_path" ]] || return 1
local line
IFS= read -r line < "$head_path" || return 1
printf '%s\n' "$line"
}
_cmux_report_tty_once() {
# Send the TTY name to the app once per session so the batched port scanner
# knows which TTY belongs to this panel.
@ -62,7 +100,7 @@ _cmux_report_tty_once() {
_CMUX_TTY_REPORTED=1
{
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &
} >/dev/null 2>&1 & disown
}
_cmux_ports_kick() {
@ -74,7 +112,7 @@ _cmux_ports_kick() {
_CMUX_PORTS_LAST_RUN=$SECONDS
{
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &
} >/dev/null 2>&1 & disown
}
_cmux_prompt_command() {
@ -123,7 +161,26 @@ _cmux_prompt_command() {
{
local qpwd="${pwd//\"/\\\"}"
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &
} >/dev/null 2>&1 & disown
fi
# Branch can change via aliases/tools while an older probe is still in flight.
# Track .git/HEAD content so we can restart stale probes immediately.
local git_head_changed=0
if [[ "$pwd" != "$_CMUX_GIT_HEAD_LAST_PWD" ]]; then
_CMUX_GIT_HEAD_LAST_PWD="$pwd"
_CMUX_GIT_HEAD_PATH="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
_CMUX_GIT_HEAD_SIGNATURE=""
fi
if [[ -n "$_CMUX_GIT_HEAD_PATH" ]]; then
local head_signature
head_signature="$(_cmux_git_head_signature "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || true)"
if [[ -n "$head_signature" && "$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE" ]]; then
_CMUX_GIT_HEAD_SIGNATURE="$head_signature"
git_head_changed=1
# Also invalidate the PR probe so it refreshes with the new branch.
_CMUX_PR_LAST_RUN=0
fi
fi
# Git branch/dirty can change without a directory change (e.g. `git checkout`),
@ -131,7 +188,7 @@ _cmux_prompt_command() {
# When pwd changes (cd into a different repo), kill the old probe and start fresh
# so the sidebar picks up the new branch immediately.
if [[ -n "$_CMUX_GIT_JOB_PID" ]] && kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]]; then
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" || "$git_head_changed" == "1" ]]; then
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
@ -154,20 +211,21 @@ _cmux_prompt_command() {
fi
} >/dev/null 2>&1 &
_CMUX_GIT_JOB_PID=$!
disown
_CMUX_GIT_JOB_STARTED_AT=$now
fi
# Pull request metadata (number/state/url):
# refresh on cwd change and periodically to avoid stale status.
# refresh on cwd change, HEAD change, and periodically to avoid stale status.
if [[ -n "$_CMUX_PR_JOB_PID" ]] && kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]]; then
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" || "$git_head_changed" == "1" ]]; then
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
fi
fi
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]] || (( now - _CMUX_PR_LAST_RUN >= 60 )); then
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" || "$git_head_changed" == "1" ]] || (( now - _CMUX_PR_LAST_RUN >= 60 )); then
if [[ -z "$_CMUX_PR_JOB_PID" ]] || ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_LAST_PWD="$pwd"
_CMUX_PR_LAST_RUN=$now
@ -197,6 +255,7 @@ _cmux_prompt_command() {
fi
} >/dev/null 2>&1 &
_CMUX_PR_JOB_PID=$!
disown
_CMUX_PR_JOB_STARTED_AT=$now
fi
fi
@ -205,6 +264,7 @@ _cmux_prompt_command() {
if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then
_cmux_ports_kick
fi
}
_cmux_install_prompt_command() {

View file

@ -45,8 +45,8 @@ typeset -g _CMUX_GIT_JOB_STARTED_AT=0
typeset -g _CMUX_GIT_FORCE=0
typeset -g _CMUX_GIT_HEAD_LAST_PWD=""
typeset -g _CMUX_GIT_HEAD_PATH=""
typeset -g _CMUX_GIT_HEAD_MTIME=0
typeset -g _CMUX_HAVE_ZSTAT=0
typeset -g _CMUX_GIT_HEAD_SIGNATURE=""
typeset -g _CMUX_GIT_HEAD_WATCH_PID=""
typeset -g _CMUX_PR_LAST_PWD=""
typeset -g _CMUX_PR_LAST_RUN=0
typeset -g _CMUX_PR_JOB_PID=""
@ -155,19 +155,6 @@ _cmux_install_winch_guard() {
}
_cmux_install_winch_guard
_cmux_ensure_zstat() {
# zstat is substantially cheaper than spawning external `stat`.
if (( _CMUX_HAVE_ZSTAT != 0 )); then
return 0
fi
if zmodload -F zsh/stat b:zstat 2>/dev/null; then
_CMUX_HAVE_ZSTAT=1
return 0
fi
_CMUX_HAVE_ZSTAT=-1
return 1
}
_cmux_git_resolve_head_path() {
# Resolve the HEAD file path without invoking git (fast; works for worktrees).
local dir="$PWD"
@ -195,27 +182,15 @@ _cmux_git_resolve_head_path() {
return 1
}
_cmux_git_head_mtime() {
_cmux_git_head_signature() {
local head_path="$1"
[[ -n "$head_path" && -f "$head_path" ]] || { print -r -- 0; return 0; }
if _cmux_ensure_zstat; then
typeset -A st
if zstat -H st +mtime -- "$head_path" 2>/dev/null; then
print -r -- "${st[mtime]:-0}"
return 0
fi
fi
# Fallback for environments where zsh/stat isn't available.
if command -v stat >/dev/null 2>&1; then
local mtime
mtime="$(stat -f %m "$head_path" 2>/dev/null || stat -c %Y "$head_path" 2>/dev/null || echo 0)"
print -r -- "$mtime"
[[ -n "$head_path" && -r "$head_path" ]] || return 1
local line=""
if IFS= read -r line < "$head_path"; then
print -r -- "$line"
return 0
fi
print -r -- 0
return 1
}
_cmux_report_tty_once() {
@ -244,6 +219,65 @@ _cmux_ports_kick() {
} >/dev/null 2>&1 &!
}
_cmux_report_git_branch_for_path() {
local repo_path="$1"
[[ -n "$repo_path" ]] || return 0
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local branch dirty_opt="" first
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
if [[ -n "$branch" ]]; then
first="$(git -C "$repo_path" status --porcelain -uno 2>/dev/null | head -1)"
[[ -n "$first" ]] && dirty_opt="--status=dirty"
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
}
_cmux_stop_git_head_watch() {
if [[ -n "$_CMUX_GIT_HEAD_WATCH_PID" ]]; then
kill "$_CMUX_GIT_HEAD_WATCH_PID" >/dev/null 2>&1 || true
_CMUX_GIT_HEAD_WATCH_PID=""
fi
}
_cmux_start_git_head_watch() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local watch_pwd="$PWD"
local watch_head_path
watch_head_path="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
[[ -n "$watch_head_path" ]] || return 0
local watch_head_signature
watch_head_signature="$(_cmux_git_head_signature "$watch_head_path" 2>/dev/null || true)"
_CMUX_GIT_HEAD_LAST_PWD="$watch_pwd"
_CMUX_GIT_HEAD_PATH="$watch_head_path"
_CMUX_GIT_HEAD_SIGNATURE="$watch_head_signature"
_cmux_stop_git_head_watch
{
local last_signature="$watch_head_signature"
while true; do
sleep 1
local signature
signature="$(_cmux_git_head_signature "$watch_head_path" 2>/dev/null || true)"
if [[ -n "$signature" && "$signature" != "$last_signature" ]]; then
last_signature="$signature"
_cmux_report_git_branch_for_path "$watch_pwd"
fi
done
} >/dev/null 2>&1 &!
_CMUX_GIT_HEAD_WATCH_PID=$!
}
_cmux_preexec() {
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
@ -265,9 +299,12 @@ _cmux_preexec() {
# Register TTY + kick batched port scan for foreground commands (servers).
_cmux_report_tty_once
_cmux_ports_kick
_cmux_start_git_head_watch
}
_cmux_precmd() {
_cmux_stop_git_head_watch
# Skip if socket doesn't exist yet
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
@ -328,6 +365,8 @@ _cmux_precmd() {
fi
# Git branch/dirty: update immediately on directory change, otherwise every ~3s.
# While a foreground command is running, _cmux_start_git_head_watch probes HEAD
# once per second so agent-initiated git checkouts still surface quickly.
local should_git=0
# Git branch can change without a `git ...`-prefixed command (aliases like `gco`,
@ -335,13 +374,13 @@ _cmux_precmd() {
if [[ "$pwd" != "$_CMUX_GIT_HEAD_LAST_PWD" ]]; then
_CMUX_GIT_HEAD_LAST_PWD="$pwd"
_CMUX_GIT_HEAD_PATH="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
_CMUX_GIT_HEAD_MTIME=0
_CMUX_GIT_HEAD_SIGNATURE=""
fi
if [[ -n "$_CMUX_GIT_HEAD_PATH" ]]; then
local head_mtime
head_mtime="$(_cmux_git_head_mtime "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || echo 0)"
if [[ -n "$head_mtime" && "$head_mtime" != 0 && "$head_mtime" != "$_CMUX_GIT_HEAD_MTIME" ]]; then
_CMUX_GIT_HEAD_MTIME="$head_mtime"
local head_signature
head_signature="$(_cmux_git_head_signature "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || true)"
if [[ -n "$head_signature" && "$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE" ]]; then
_CMUX_GIT_HEAD_SIGNATURE="$head_signature"
# Treat HEAD file change like a git command — force-replace any
# running probe so the sidebar picks up the new branch immediately.
_CMUX_GIT_FORCE=1
@ -381,16 +420,7 @@ _cmux_precmd() {
_CMUX_GIT_LAST_PWD="$pwd"
_CMUX_GIT_LAST_RUN=$now
{
local branch dirty_opt=""
branch=$(git branch --show-current 2>/dev/null)
if [[ -n "$branch" ]]; then
local first
first=$(git status --porcelain -uno 2>/dev/null | head -1)
[[ -n "$first" ]] && dirty_opt="--status=dirty"
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
_cmux_report_git_branch_for_path "$pwd"
} >/dev/null 2>&1 &!
_CMUX_GIT_JOB_PID=$!
_CMUX_GIT_JOB_STARTED_AT=$now
@ -488,7 +518,12 @@ _cmux_fix_path() {
add-zsh-hook -d precmd _cmux_fix_path
}
_cmux_zshexit() {
_cmux_stop_git_head_watch
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec _cmux_preexec
add-zsh-hook precmd _cmux_precmd
add-zsh-hook precmd _cmux_fix_path
add-zsh-hook zshexit _cmux_zshexit