jcode Spawn Hook 完整指南:用 tmux、kitty、zellij 接管会话窗口的创建与路由
【免费下载链接】jcodeThe most RAM efficient harness项目地址: https://gitcode.com/GitHub_Trending/jcod/jcode
本文以 docs/SPAWN_HOOK.md 为核心骨架,结合 jcode 仓库中 crates/jcode-terminal-launch/src/lib.rs、crates/jcode-base/src/terminal_launch.rs 与 crates/jcode-app-core/src/session_launch.rs 的源码实现,系统讲解 jcode 的spawn hook(生成钩子)机制:它让外部程序接管"有头会话"的终端窗口创建,从而把 swarm 代理、resume-in-new-terminal、self-dev 等会话精确路由到 tmux 窗格、kitty 标签页或自定义脚本指定的位置。读完本文,你将掌握 spawn hook 的配置方式、调用契约、元数据环境变量、多终端路由原理,并拿到可直接复制的 tmux / kitty / zellij / 自定义路由脚本实战方案。
Spawn Hook 解决什么问题
jcode 会在多条流程中打开新的终端窗口:
- swarm 代理生成(
swarm spawn,且spawn_mode=visible); - resume-in-new-terminal(在新终端中恢复会话);
- self-dev 会话;
- 重启恢复(restart restores);
- jade relay 启动。
默认情况下,jcode 自行探测系统中已安装的终端模拟器(kitty、wezterm、alacritty、gnome-terminal……),并打开一个新的操作系统窗口。从源码看,这个"内置探测"的候选顺序在 crates/jcode-terminal-launch/src/lib.rs 中定义:在 tmux 内优先使用当前客户端的右侧分屏窗格,随后按 herdr、handterm、zellij、screen、kitty、wezterm、alacritty、ghostty,再到 gnome-terminal、konsole、xterm、foot 依次尝试。
spawn hook 让一个外部程序完全接管这次生成,由它决定会话出现在哪里、以何种形态出现:一个 tmux 窗格、一个 kitty 标签页、一个 zellij 窗格、一个 wrapper 应用(如 herd)里的标签页,甚至是某个特定显示器/工作区。
快速配置:配置文件与环境变量
方式一:config.toml
在~/.jcode/config.toml的[terminal]段配置:
# ~/.jcode/config.toml [terminal] spawn_hook = "tmux new-window"方式二:环境变量
export JCODE_SPAWN_HOOK="tmux new-window" # 空值用于禁用配置文件中的 hook: export JCODE_SPAWN_HOOK=环境变量永远优先于配置文件。这一优先级在 crates/jcode-base/src/config/env_overrides.rs 中实现:环境变量存在时直接覆盖self.terminal.spawn_hook,且显式空值会把配置文件的 hook 置为None(即禁用)。
默认配置文件模板 crates/jcode-base/src/config/default_file.rs 中给出了完整注释,包括JCODE_SPAWN_*元数据变量的说明和三个示例(tmux new-window、kitty @ launch --type=tab --、~/bin/jcode-spawn-router)。
调用契约:hook 如何被执行
当发生一次有头生成且配置了 hook 时,jcode 执行:
<spawn_hook> <jcode-binary> <args...>契约要点如下:
- Shell 风格解析,但直接执行:hook 命令行按 shell 风格解析(引号和反斜杠转义都可用),但不经过 shell 直接 exec。解析器实现在 crates/jcode-terminal-launch/src/lib.rs(
parse_hook_command):空白分隔参数、单双引号分组、双引号内反斜杠转义;空输入、未闭合引号、结尾转义都会报错。 - jcode 二进制与完整参数列表作为追加的 argv:即大家熟悉的
$TERMINAL -e <cmd>约定。build_hook_spawn_command在 crates/jcode-terminal-launch/src/lib.rs 中把解析出的 hook 前缀参数与command.program、command.args拼在一起。 - 工作目录:hook 的工作目录是会话工作目录(
cwd)。 - 分离运行:hook 进程被 detach,jcode 不等待它结束。
- 启动失败回退:如果 hook 无法启动(二进制缺失、解析错误),jcode 记录警告并回退到内置终端探测。
源码层面,crates/jcode-base/src/terminal_launch.rs 的spawn_via_hook还做了更细的处理:hook 启动后有2 秒启动窗口——若 hook 在这期间以非零退出,立即判定失败并回退;若 hook 存活超过 2 秒,则视为启动成功,随后在后台线程异步收割该进程(因为长期运行的 hook 可能有意持有自己的终端进程)。这一行为有对应测试 crates/jcode-base/src/terminal_launch.rs:exit 1的 hook 会触发回退并报出hook exited with错误。
元数据环境变量
hook(以及内置回退路径启动的终端)都会收到以下环境变量:
| 变量 | 含义 |
|---|---|
JCODE_SPAWN_KIND | 生成原因:swarm-agent、resume、selfdev、restart、jade-relay |
JCODE_SPAWN_SESSION_ID | 该窗口将运行的 jcode 会话 |
JCODE_SPAWN_TITLE | 建议的窗口/标签页标题(含会话图标与名称) |
JCODE_SPAWN_CWD | 会话工作目录 |
JCODE_SPAWN_PROGRAM | 要执行的 jcode 二进制路径 |
JCODE_SPAWN_COMMAND | 完整命令行(shell 转义),供需要单个 shell 字符串的 hook 使用 |
JCODE_SPAWN_SWARM_ID | (swarm 生成时)agent 加入的 swarm |
JCODE_SPAWN_COORDINATOR_SESSION_ID | (swarm 生成时)发起生成的协调者会话 |
JCODE_FRESH_SPAWN | 当本次生成是新窗口交接时为1 |
这些变量由spawn_metadata_env统一构造(crates/jcode-terminal-launch/src/lib.rs),其中JCODE_SPAWN_COMMAND使用shell_command对每个参数做sh_escape单引号转义,保证拼接后的字符串可以被bash -lc之类的 shell 安全消费。额外的 swarm 元数据(JCODE_SPAWN_SWARM_ID等)通过TerminalCommand::extra_env最后追加,发生键冲突时后者胜出。
客户端终端环境:多终端路由
jcode 的 server 进程是长驻的:它在启动时捕获一次终端标识环境变量(ZELLIJ_SESSION_NAME、TMUX、DISPLAY、KITTY_WINDOW_ID……)。当你之后在新的终端/tmux/zellij 会话中连接客户端到同一个 server 时,server 持有的这些副本已经过时,于是由 server 执行的 spawn hook 会错误地定位到旧终端。
为解决这个问题(issue #405),每个连接的客户端会快照自己的终端标识环境变量并发送给 server。spawn hook 运行时,server 会重新导出发起请求的客户端的值,使 hook 跟随用户当前实际所在的终端:
- 原生变量(如
ZELLIJ_SESSION_NAME)被客户端的值覆盖,直接读取它的 hook 会定位到正确的会话; - 同时导出
JCODE_CLIENT_<NAME>别名(如JCODE_CLIENT_ZELLIJ_SESSION_NAME、JCODE_CLIENT_TMUX、JCODE_CLIENT_DISPLAY),让 hook 能显式区分客户端终端与 server 终端。
覆盖的键位包括终端复用器(zellij、tmux、screen)、终端模拟器(kitty、wezterm、ghostty、alacritty、iTerm、Windows Terminal、handterm)以及显示服务器(DISPLAY、WAYLAND_DISPLAY)。完整清单CLIENT_TERMINAL_ENV_VARS见 crates/jcode-terminal-launch/src/lib.rs,其中还包含 herdr 相关变量(HERDR_ENV、HERDR_PANE_ID等)。只有客户端实际设置的变量才会被转发(snapshot_client_terminal_env,crates/jcode-terminal-launch/src/lib.rs)。
apply_client_terminal_env(crates/jcode-terminal-launch/src/lib.rs)的注释点明了一个关键细节:先移除全部已知键再写入客户端快照——对共享 server 而言,空的客户端快照绝不能泄露"碰巧启动 server 的那个窗格"的身份。并发客户端之间通过 task-local 隔离,互不污染,这在 crates/jcode-base/src/hooks.rs 的并发测试中得到了验证(两个客户端分别携带HERDR_PANE_ID=pane-left与pane-right,并行执行互不干扰)。
实战示例
tmux:每个 agent 一个窗口
[terminal] spawn_hook = "tmux new-window"当 jcode 探测到发起请求的客户端位于 tmux 内时,它的内置启动器默认会把有头生成放进请求方TMUX_PANE的右侧分屏窗格(覆盖/split、/fork、resume-in-new-terminal、self-dev 与可见 agent 生成)。若想覆盖这种自动分屏行为,可以用tmux new-window <jcode> --resume ses_x——命令会跑在当前 tmux server 的一个新窗口中。若想显式保留右侧分屏行为:
[terminal] spawn_hook = "tmux split-window -h"注意:内置的 tmux 右分屏实现(crates/jcode-terminal-launch/src/lib.rs)会额外传入-t <TMUX_PANE>精确定位请求方窗格;而配置 hook 时该细节由你的命令自行决定。
kitty:每个 agent 一个标签页(远程控制)
[terminal] spawn_hook = "kitty @ --to unix:/tmp/kitty.sock launch --type=tab --"自定义路由脚本
需要完全控制(放置位置、标题、swarm 与 resume 的差异化路由)时,把 hook 指向一个脚本:
[terminal] spawn_hook = "~/bin/jcode-spawn-router"#!/usr/bin/env bash # ~/bin/jcode-spawn-router # argv: the jcode command to run ("$@"). Env: JCODE_SPAWN_* metadata. case "$JCODE_SPAWN_KIND" in swarm-agent) # Swarm workers as tmux panes in a window named after the swarm. tmux new-window -n "swarm:${JCODE_SPAWN_SWARM_ID:0:8}" "$@" 2>/dev/null \ || tmux split-window "$@" ;; *) # Everything else as a normal terminal window. kitty --title "$JCODE_SPAWN_TITLE" -e "$@" & ;; esac重要:hook 启动后以非零退出且未启动任何东西,不会触发内置回退——jcode 只在 hook进程无法启动时回退。因此路由脚本必须自己处理回退逻辑,如上例中的|| tmux split-window。这一点在 crates/jcode-base/src/terminal_launch.rs 的 2 秒启动窗口逻辑中体现:hook 存活超过启动窗口即视为成功,之后它自己退出与否不再影响 jcode。
单 shell 字符串消费者
有些启动器想要一条 shell 命令字符串而非 argv,此时用$JCODE_SPAWN_COMMAND:
#!/usr/bin/env bash zellij action new-pane -- bash -lc "$JCODE_SPAWN_COMMAND"程序化发现:wrapper 集成
包装 jcode 的程序(如 herd 风格的会话管理器)可以在其启动的jcodeserver 进程环境中设置JCODE_SPAWN_HOOK。此后 server 执行的每一次有头生成——包括协调者通过 socket 协议请求的 swarm agent——都会路由到 wrapper 的 hook。这让 wrapper 无需改动 jcode 源码即可统一接管所有窗口放置。
Focus Hook:把已有会话窗口带到前台
jcode 想把已存在的会话窗口带到前台时(例如启动 self-dev 窗口之后),在 X11 上默认做一次尽力而为的 wmctrl/xdotool 标题搜索。但这种方式在 Wayland 下、以及终端复用器内部都不奏效——而且既然 wrapper 拥有放置权,焦点控制也应该由它负责:
[terminal] spawn_hook = "tmux new-window" focus_hook = "~/bin/jcode-focus" # env: JCODE_FOCUS_SESSION_ID, JCODE_FOCUS_TITLE#!/usr/bin/env bash # ~/bin/jcode-focus tmux select-window -t "$(tmux list-windows -F '#{window_id} #{window_name}' \ | grep -F "$JCODE_FOCUS_TITLE" | head -1 | cut -d' ' -f1)"环境变量覆盖为JCODE_FOCUS_HOOK(空值禁用配置文件中的 hook)。若 hook 无法启动,jcode 回退到内置焦点路径。
源码层面,crates/jcode-app-core/src/session_launch.rs 实现了focus_session_via_hook与focus_session_window_best_effort:先尝试配置的 focus hook(携带JCODE_FOCUS_SESSION_ID与JCODE_FOCUS_TITLE,并转发请求客户端的终端环境),失败后再执行内置的wmctrl -a/xdotool search --name ... windowactivate尽力而为回退。focus_hook与spawn_hook的配置定义集中在 crates/jcode-config-types/src/lib.rs 的TerminalConfig中,二者配套使用才能实现"谁放置窗口、谁负责聚焦"的完整闭环。
与生命周期 Hook 的关系
spawn hook 与[hooks]生命周期钩子(turn_start、turn_end、session_start、pre_tool等,见 crates/jcode-base/src/hooks.rs)共享同一种命令解析与执行约定(shell 风格解析、直接执行、JCODE_HOOK_*元数据环境变量),但职责不同:生命周期钩子观察/门控 agent 行为,spawn hook 专管"会话窗口出现在哪里"。二者可以独立配置、独立使用。
小结
spawn hook 把 jcode 的窗口放置策略完全外置化:无论是追求"一个 agent 一个 tmux 窗口"的隔离工作流,还是 kitty 标签页式轻量并行,抑或 herd 这类 wrapper 的深度集成,都可以通过一行spawn_hook配置(或环境变量)实现,并且配套的JCODE_SPAWN_*元数据、客户端终端环境转发(issue #405)与 focus hook 保证了 hook 总是知道"谁发起的、要放在哪个终端、该怎么聚焦"。建议进一步阅读仓库中的 docs/SPAWN_HOOK.md(本文依据)、docs/HERDR.md(herdr 集成场景)、crates/jcode-terminal-launch/src/lib.rs(解析、快照与内置启动实现)以及 crates/jcode-base/src/terminal_launch.rs(hook 启动与回退逻辑)来深入理解各平台的细节差异。
【免费下载链接】jcodeThe most RAM efficient harness项目地址: https://gitcode.com/GitHub_Trending/jcod/jcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考