把tmux里的code_重新变回code

我经常从VS Code Remote的集成终端进入一个长期运行的tmux session。集成终端里用code,进入tmux以后却要改用我自己写的code_。偶尔会按错,不够优雅,正好现在AI挺方便的,我决定尝试把它们统一成code

我原来的办法

VS Code Remote会把CLI放在~/.vscode-server/cli下面,也会在/run/user/$UID中创建形如vscode-ipc-*的Unix socket。code通过环境变量VSCODE_IPC_HOOK_CLI知道应该把请求发给哪个VS Code窗口。Remote CLI源码把这个变量读作cliPipe,随后通过它向远端窗口发送请求。

我以前从别处抄了一个名为code_的脚本(唉还是大家都用stack overflow的年代)。它先找到最近更新的Remote CLI,再按照修改时间枚举IPC socket,逐个尝试:

#!/usr/bin/env bash

max_retry=10

for i in $(seq 1 "$max_retry"); do
    script=$(fd -t f '^code$' "$HOME/.vscode-server/cli" -X ls -t | head -n 1)

    if [[ -z $script ]]; then
        echo "VS Code remote script not found" >&2
        exit 1
    fi

    socket=$(ls -t /run/user/"$UID"/vscode-ipc-* 2>/dev/null | head -n "$i")

    if [[ -z $socket ]]; then
        echo "VS Code IPC socket not found" >&2
        exit 1
    fi

    VSCODE_IPC_HOOK_CLI=$socket "$script" "$@" && exit 0
done

echo "Failed to find valid VS Code window" >&2
exit 1

这个思路假设最新的socket最可能属于我当前使用的窗口。如果失败,脚本继续尝试更早的socket。

这里还藏着一个直接的实现错误:head -n "$i"在第二次循环会返回两行,后面依次返回更多行(哈哈这个是GPT找出来的我都不知道有这回事,反正之前一直work)。它没有选出第i个socket。即使把它改成真正逐项选择,这套办法仍然解决不了核心问题。

统一名字不能只写一个alias

最直接(但丑陋)的做法是给code_加一个名为code的alias。这样只藏住了第二个名字,底下仍然保留着扫描CLI和socket的脚本,这当然不是我要的优雅实现。

我想让同一个code在集成终端和tmux中采用同一套行为,要做到这一点,我得先弄清楚Remote扩展提供的code怎样定位CLI和VS Code窗口。原生不用tmux的情况下VS Code在创建集成终端时已经通过环境变量给出了这两项信息。

VSCODE_IPC_HOOK_CLI属于启动当前shell的VS Code窗口。两个workspace同时连接同一台远端机器时,它们会各自持有一个socket,它的修改时间只说明文件最近发生过什么(一般来讲就是在vscode工作区做了什么操作),不是100%正确的当前工作区的指标,很可能我在B工作区做了什么操作之后立刻跑到A工作区的tmux里面敲code_命令,这种偶尔的情况就会出问题。总之,这个方法没有保存“这个tmux session从哪个VS Code窗口进入”这条关系。

tmux本来就能保存这条关系

想了半天是不是有什么特殊Hook,GPT调查一下才发现tmux直接有这个功能。

tmux的update-environment选项会在创建或attach session时,从客户端复制指定的环境变量。只要我从目标VS Code窗口的集成终端执行tmux attach,tmux就能把这个窗口的socket保存到对应session中。VS Code Remote的上游issue #2763也记录了这种配置方式(2020年的老东西,看来我当年查的时候没查到这个,只查到了猴版code_),以及现有pane需要主动读取session environment这一点。

我在~/.tmux.conf中加入:

set -ga update-environment VSCODE_IPC_HOOK_CLI
set -ga update-environment VSCODE_GIT_ASKPASS_NODE

第二个变量VSCODE_GIT_ASKPASS_NODE我拿来解决CLI路径问题。它指向当前VS Code Server安装中的Node可执行文件。使用标准Remote Server目录结构,可以从它得到同一套安装中的CLI:

code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"

socket负责定位窗口,askpass Node路径负责定位与当前Server安装配套的code。两者都来自创建集成终端的VS Code进程,不需要扫描目录,也不需要按照时间猜测。

加载tmux配置:

tmux source-file ~/.tmux.conf

配置只会在下一次创建或attach session时从客户端取得变量。因此我先离开tmux,再从目标VS Code集成终端重新attach:

tmux attach -t my-session

让已有shell取得更新后的值

tmux会更新session environment,但已经运行的shell不会自动修改自己的environment。hmmm就是说我attach上这个tmux之后里面的zsh不会自动从tmux重读这个值,所以我得让shell在执行命令前从tmux读取最新值。

我的zsh配置使用preexec hook:

autoload -Uz add-zsh-hook

_sync_tmux_vscode_env() {
    [[ -n $TMUX ]] || return

    local name entry
    for name in VSCODE_IPC_HOOK_CLI VSCODE_GIT_ASKPASS_NODE; do
        entry=$(tmux show-environment "$name" 2>/dev/null) || {
            unset "$name"
            continue
        }

        case $entry in
            "$name="*) export "$entry" ;;
            -"$name")  unset "$name" ;;
        esac
    done
}

add-zsh-hook preexec _sync_tmux_vscode_env

bash可以通过PROMPT_COMMAND做同样的事:

_sync_tmux_vscode_env() {
    [[ -n $TMUX ]] || return

    local name entry
    for name in VSCODE_IPC_HOOK_CLI VSCODE_GIT_ASKPASS_NODE; do
        entry=$(tmux show-environment "$name" 2>/dev/null) || {
            unset "$name"
            continue
        }

        case $entry in
            "$name="*) export "$entry" ;;
            -"$name")  unset "$name" ;;
        esac
    done
}

PROMPT_COMMAND="_sync_tmux_vscode_env${PROMPT_COMMAND:+;$PROMPT_COMMAND}"

最后,我直接定义code成如下function,然后就可以愉快删掉code_啦:

code() {
    local code_cli
    code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"

    if [[ -z $VSCODE_GIT_ASKPASS_NODE || ! -x $code_cli ]]; then
        echo "VS Code Remote CLI not found" >&2
        return 127
    fi

    "$code_cli" "$@"
}

zsh和bash都能使用这段函数。每次执行code前,shell先取得当前tmux session保存的变量,然后调用对应Server安装中的CLI。CLI再通过VSCODE_IPC_HOOK_CLI联系正确的窗口。

验证

重新attach后,我先检查tmux session中的值:

tmux show-environment VSCODE_IPC_HOOK_CLI
tmux show-environment VSCODE_GIT_ASKPASS_NODE

再检查socket和CLI:

test -S "$VSCODE_IPC_HOOK_CLI" && echo "IPC socket is live"

code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"
test -x "$code_cli" && printf '%s\n' "$code_cli"

最后执行:

source ~/.zshrc
code .

目录应该在刚才用于attach tmux的VS Code窗口中打开。换到另一个workspace时,我从那个窗口重新attach对应session,tmux就会记录那个窗口提供的变量。

结论

我原来的脚本同时做了两次时间猜测:从~/.vscode-server/cli里选最近的CLI,再从/run/user/$UID里选最近的socket。它在只有一个窗口时通常能工作,多窗口和频繁切换workspace以后就失去了明确依据。

现在,VS Code在集成终端中提供窗口和Server安装信息,tmux在attach时保存它们,shell在运行命令前读取它们。code只负责调用CLI。这个流程保留了VS Code窗口、tmux session和workspace之间原有的对应关系。


声明:人类活动,但由Codex记录内容,最终人类进行些微修订后发布。


Last modified on 2026-07-20