herdr worktree 弹窗 Esc 失效排查与自救

事件时间:2026-07-01 herdr 版本:v0.6.8(Rust,static-pie ELF,/home/zbc/.local/bin/herdr) 受影响仓库:/home/zbc/pangu/GeneralAndroid 关联调研 → herdr-终端AI Agent多路复用器深度调研 类型:复盘案例(case)


TL;DR(太长不看)

  • 现象:herdr 执行创建 worktree 时弹出 new worktree 确认框(底部提示 create and open / esc cancel),按 Esc 完全无反应Ctrl+C / Ctrl+D 在弹窗里直接变成字母 c / d,前端 UI 卡死。
  • 根因:系统输入法为 fcitxGTK_IM_MODULE=fcitx / QT_IM_MODULE=fcitx / XMODIFIERS=@im=fcitx)。fcitx 在激活态会拦截 Esc 与部分 Ctrl 组合键(用于清候选/状态切换),不透传给 herdr 的 TUI 弹窗组件,导致”按键像消失了一样”。终端与 Ctrl 键本身正常。
  • 解法:另开终端用 PID 精确 kill 卡死进程树 → 清理半成品 worktree → 下次进 herdr 弹窗前先切英文输入法。
  • 一句话经验:TUI 程序”快捷键失灵但终端里 Ctrl+C 正常”,先查输入法拦截,不要先怀疑程序本身。

一、问题现象

执行 herdr 创建 worktree,弹出确认弹窗,UI 内容如下:

new worktree
branch
_                                   ← 文本输入框(光标在此)
checkout
/home/zbc/.herdr/worktrees/GeneralAndroid/worktree creating.

  create and open      esc cancel    ← 两个操作按钮

弹窗自己声明了 esc cancel(Esc = 取消)。但实际表现:

按键预期实际
Esc关闭弹窗完全没动静,弹窗不消失,输入框也不出现乱码
Ctrl+C中断/取消直接在输入框打出字母 c
Ctrl+DEOF/取消直接在输入框打出字母 d

前端 UI 完全卡死,无法退出。(事后排查发现)herdr 后台 server 仍在继续创建 worktree,并已 fork 出多个下载子进程——这是 herdr 作为 tmux 式多路复用器的架构特性(见根因 §三)。


二、排查过程与根因证据

2.1 第一步:证明终端和 Ctrl 键本身没问题

在普通 shell 里执行:

zbc@zbc-OptiPlex-7080:~$ cat        # 回车进入 cat
^C                                   # 按 Ctrl+C,显示 ^C 并退出 cat
zbc@zbc-OptiPlex-7080:~$             # 回到提示符

结论Ctrl+C 在终端层正常产生 ^C(SIGINT),说明:

  1. 物理按键和 Ctrl 修饰键工作正常;
  2. 终端模拟器正确把 Ctrl 编码后发给前台程序;
  3. 问题不在终端,只在 herdr 的弹窗里。

2.2 第二步:定位到输入法层(决定性证据)

检查输入法环境:

$ echo "GTK=$GTK_IM_MODULE QT=$QT_IM_MODULE XMOD=$XMODIFIERS"
GTK=fcitx QT=fcitx XMOD=@im=fcitx
 
$ ls /usr/bin/ | grep -iE 'ibus|fcitx|sogou'
fcitx
fcitx4-config
fcitx-autostart
fcitx-config-gtk3
fcitx-configtool

系统使用 fcitx 输入法框架。这与现象完全吻合:

现象fcitx 行为解释
Esc 完全没动静(无乱码)fcitx 在激活态(有候选词 / 中文输入进行中)会消费 Esc,用于”清空候选词 / 回到英文状态”,不透传给下层 TUI。herdr 根本收不到按键事件,所以连乱码都没有。
Ctrl+C 变字母 cfcitx 拦截/改写了 Ctrl 组合键的处理,只把字面字符 c 透传进 herdr 的文本输入框。
主对话界面 Ctrl+C 能用herdr 主输入框与弹窗输入框是不同组件;且焦点/输入法状态在弹窗打开瞬间可能不同,导致拦截表现差异。

置信度说明:这是”高置信度最可能根因”。严格意义上没有做”切到英文输入法后 Esc 是否恢复”的对照实验(因为当时优先解决卡死)。验证方法见 §四 4.1。fcitx 拦截 Esc 是其设计行为,并非 bug;herdr 作为 TUI 程序对此无能为力。

2.3 第三步:摸清 herdr 在卡死期间到底干了什么

卡死期间用 pgrep -fa 查看 herdr 进程树,发现前端 UI 虽然卡死,后台却在”静默干活”:

PID    进程
12100  /home/zbc/.local/bin/herdr server          ← 后台守护进程(tmux 式 server)
19463  herdr                                       ← 前端 TUI(卡住的弹窗就是它)
19282  git -C /home/zbc/pangu/GeneralAndroid worktree add -b worktree/green-river-87c3  \
           /home/zbc/.herdr/worktrees/GeneralAndroid/worktree-green-river-87c3 HEAD
20373  python3 .../download_assets.py --all       ← 拉取项目资源
20418  mc cp uploadrw/.../AGENT使用分享.webm       ┐
20453  mc cp uploadrw/.../Franklin1 GPU内存统计.webm │ 4 个 minio client
20484  mc cp uploadrw/.../系统UI内存泄露报告.webm    │ 正在下载大文件
23033  mc cp uploadrw/.../libfrida-core.a          ┘

关键认知:herdr 是 Rust 写的 tmux 式多路复用器(详见 herdr-终端AI Agent多路复用器深度调研),采用 server + client 架构:

  • 前端 herdr(PID 19463)只负责渲染 UI(弹窗)和接收键盘事件;
  • 后台 herdr server(PID 12100)才是真正执行 worktree 创建、调度下载的实体;
  • 二者解耦——所以前端 UI 被输入法卡死,后台 server 仍按既定流程继续 git worktree add + 拉资源

这导致一个隐蔽副作用:用户以为”弹窗还在等确认,什么都没发生”,实际上 worktree 已经在被创建、大文件已经在被下载。直接拔终端会在仓库里留下半成品 worktree + 残留分支 + 不完整资源


三、本次实际执行的解决步骤

3.1 杀掉卡死的进程树(用 PID 精确 kill)

kill 12100 19463 19282 20373 20418 20453 20484 23033

⚠️ 为什么不用 pkill -f herdr 这是本次最重要的一个坑:

  • pkill -f 匹配的是完整命令行,凡是命令行里含字符串 herdr 的进程都会被杀;
  • worktree 目录路径是 /home/zbc/.herdr/worktrees/...路径里就含 herdr,于是 git worktree addpython download_assets.py、4 个 mc cp 这些子进程的命令行全部含 .herdr,都会被一并命中——这倒正好是我们想要的;
  • 但更危险的是:你正在终端里执行的那条 pkill -f herdr 命令本身、以及包裹它的 bash -c '...herdr...' shell,命令行也含 herdrpkill 会杀到自己的执行 shell,命令可能被中途打断,行为不可预期。

因此优先用 PID 列表 kill,最精确、无副作用。PID 可通过 pgrep -fa herdrps -ef | grep herdr 提前拿到。

确认全部退出:

$ ps -p 12100,19463,19282,20373,20418,20453,20484,23033 -o pid,stat,cmd --no-headers
(无输出 = 这些 PID 均已不存在)

3.2 清理半成品 worktree

杀进程后,仓库里留下了卡死过程的副产品:

$ git -C /home/zbc/pangu/GeneralAndroid worktree list
/home/zbc/pangu/GeneralAndroid                                       cec17583 [master]
/home/zbc/.herdr/worktrees/GeneralAndroid/worktree-green-river-87c3  cec17583 [worktree/green-river-87c3]   ← 残留
 
$ git -C /home/zbc/pangu/GeneralAndroid branch --list 'worktree/*'
+ worktree/green-river-87c3 残留分支

按顺序清理(必须先移除 worktree,否则被 checkout 占用的分支删不掉):

# 1. 移除 worktree 注册(--force 是因为目录里有未跟踪的下载文件)
git -C /home/zbc/pangu/GeneralAndroid worktree remove --force \
    /home/zbc/.herdr/worktrees/GeneralAndroid/worktree-green-river-87c3
 
# 2. 删除残留分支
git -C /home/zbc/pangu/GeneralAndroid branch -D worktree/green-river-87c3

3.3 验证仓库恢复干净

$ git -C /home/zbc/pangu/GeneralAndroid worktree list
/home/zbc/pangu/GeneralAndroid  cec17583 [master]      ← 只剩主仓库,干净
$ git -C /home/zbc/pangu/GeneralAndroid branch --list 'worktree/*'
(空) 无残留分支

四、后续遇到同样问题的解决办法

4.1 预防(优先级最高)

进入 herdr 任何弹窗之前,先切到英文输入法:

  • fcitx 默认切换键:Ctrl+Space(中/英切换)或 Shift(临时切英文);
  • 或直接在 fcitx 配置里关掉对终端类程序的接管(见 §五 5.3);
  • 习惯:用 TUI 程序(herdr / Claude Code / lazygit / helix …)一律先确认输入法是英文

验证 fcitx 假设(如果你想确认根因,而不是直接动手):

  1. 切到英文输入法;
  2. 重新触发 herdr worktree 弹窗;
  3. 按 Esc——若能正常关闭,则 fcitx 拦截假设成立。

4.2 自救(已经卡住时)

按”破坏性从小到大”排序:

方案 A —— 另开终端,精确杀 herdr(推荐)

# 只杀 herdr 可执行,命令行精确匹配,绝不误伤
pkill -f '/home/zbc/.local/bin/herdr'

注意:这条命令匹配的是 /home/zbc/.local/bin/herdr(server 进程的完整路径),不会匹配到那些命令行里只是路径含 .herdr/worktrees/... 的 git/python/mc 子进程,也不会匹配到自己正在执行的 shell(因为 shell 命令行里是 pkill -f '/home/zbc/.local/bin/herdr'……等等,这句命令行本身也含这个字符串)。

更稳妥的写法是先查再按 PID 杀:

pgrep -af '/home/zbc/.local/bin/herdr'    # 拿到 PID
kill <PID>                                 # 精确杀

方案 B —— Ctrl + \(SIGQUIT)

命令行里既然 Ctrl 生效,在 herdr 弹窗里试 Ctrl+\(反斜杠),SIGQUIT 比 SIGINT 更强,常能把 TUI 程序直接打断退出。(本次未实测,因为优先用了方案 A。)

方案 C —— Ctrl+Z 挂起 + kill

# 卡住的终端里:
Ctrl+Z            # SIGTSTP 挂起 herdr,回到 shell
kill %1           # 杀掉后台 job

方案 D —— 暴力

直接关闭终端窗口。100% 能出来,但可能留下半成品 worktree(需按 §4.3 清理)。

4.3 清理半成品 worktree(通用)

每次强杀 herdr 后,都要检查并清理仓库里的半成品:

REPO=/home/zbc/pangu/GeneralAndroid
 
# 1. 检查残留
git -C $REPO worktree list
git -C $REPO branch --list 'worktree/*'
ls /home/zbc/.herdr/worktrees/GeneralAndroid/
 
# 2. 移除所有残留 worktree(路径从上面 worktree list 里取)
git -C $REPO worktree remove --force <残留worktree路>
 
# 3. 如果 worktree 目录已经被强删但 git 还登记着,用 prune
git -C $REPO worktree prune
 
# 4. 删除残留分支
git -C $REPO branch -D worktree/<>

五、经验总结

5.1 TUI”快捷键失灵”的诊断顺序

遇到 TUI 程序(herdr / Claude Code / lazygit / vim / helix …)某个快捷键不响应,按这个顺序排查,避免误判:

  1. 输入法(最常被忽略,也最常见):echo $GTK_IM_MODULE $QT_IM_MODULE $XMODIFIERS;切英文再试。
  2. 终端 Ctrl 键cat + Ctrl+C,看是否产生 ^C。能 = 终端正常;打出字母 = 终端/Ctrl 键码问题。
  3. tmux/screen 前缀键冲突:是否在多路复用器里,前缀键吃掉了组合键。
  4. 终端模拟器键绑定:某些终端把 Ctrl+C/Ctrl+V 绑成复制粘贴,或把 Esc 当 Alt 前缀。
  5. 最后才怀疑程序本身:前 4 项都正常,再去查程序的键映射配置或提 issue。

本次一开始误判为”Claude Code 弹窗”(用户最初的 “herdr” 被当成拼写错误),浪费了几轮。教训:用户消息里的陌生词先当专有名词查证(路径 /home/zbc/.herdr/ 一秒钟就证伪了误判)。

5.2 pkill -f 的三个陷阱

pkill -f <pattern> 匹配完整命令行,由此带来三个坑:

  1. 路径误伤:pattern 若是某目录名片段,所有命令行里带该路径的进程都会被杀(本次的 .herdr 路径)。
  2. 自杀:你正在执行的 pkill -f xxx 命令、以及它的 bash -c 外壳,命令行里就含 xxx,可能杀到自己导致命令中断。
  3. 子进程连带:父进程被杀时,命令行含 pattern 的子进程也会被一起命中。

结论:在交互式排障场景,优先用 PID kill;必须用 pkill 时,pattern 尽量写完整可执行路径(如 /home/zbc/.local/bin/herdr)而非裸名。

5.3 让 fcitx 不再干扰终端程序

根治思路(任选其一,按需采用):

  • 临时:用终端前 Ctrl+Space 切英文。
  • 按程序豁免:fcitx 配置工具里,把终端模拟器(gnome-terminal / konsole / alacritty 等)加入”不使用输入法”或设置默认英文。
  • 环境变量级:在终端模拟器的启动环境里 export GTK_IM_MODULE= QT_IM_MODULE= XMODIFIERS=(仅对终端禁用输入法,影响最小)。可写入 ~/.config/alacritty/alacritty.toml 的 env 段,或包一层 wrapper 脚本。
  • 换 fcitx5 + 调整 Esc 行为:fcitx5 的”在程序内禁用”和 Esc 透传策略更可控。

5.4 herdr 架构认知(来自本次实战)

  • herdr = server(守护进程) + client(前端 TUI) 的 tmux 式多路复用器;
  • 前端卡死 ≠ 整体卡死:server 会继续执行已下发的任务(本次是 worktree 创建 + 资源下载);
  • 强杀前端后务必检查 git worktree list~/.herdr/worktrees/,清理半成品;
  • 详细架构见 herdr-终端AI Agent多路复用器深度调研

附录 A:环境信息快照

项目
机器zbc-OptiPlex-7080
herdr 版本v0.6.8
herdr 二进制/home/zbc/.local/bin/herdr(13MB,static-pie ELF,Rust)
输入法fcitx(GTK_IM_MODULE=fcitx / QT_IM_MODULE=fcitx / XMODIFIERS=@im=fcitx
受影响仓库/home/zbc/pangu/GeneralAndroid
worktree 根目录/home/zbc/.herdr/worktrees/
卡死 worktreeworktree-green-river-87c3 / 分支 worktree/green-river-87c3(已清理)

附录 B:一键自救脚本

把以下内容存为 ~/bin/herdr-kill-and-clean.sh,下次卡住直接跑(路径按需改):

#!/usr/bin/env bash
# 用途:杀掉卡死的 herdr 进程树,并清理 GeneralAndroid 的半成品 worktree
set -u
REPO=/home/zbc/pangu/GeneralAndroid
WT_ROOT=/home/zbc/.herdr/worktrees/GeneralAndroid
 
echo "==> 1. 杀掉 herdr 进程树"
PIDS=$(pgrep -f '/home/zbc/.local/bin/herdr')
[ -n "$PIDS" ] && kill $PIDS && echo "已 kill: $PIDS" || echo "无 herdr 进程"
 
echo "==> 2. 清理半成品 worktree"
for wt in $(git -C "$REPO" worktree list --porcelain | awk '/^worktree / && $2 != "'"$REPO"'"{print $2}'); do
  git -C "$REPO" worktree remove --force "$wt" && echo "移除 worktree: $wt"
done
git -C "$REPO" worktree prune
 
echo "==> 3. 删除残留 worktree 分支"
for b in $(git -C "$REPO" branch --list 'worktree/*' | sed 's/[*+ -]//g'); do
  git -C "$REPO" branch -D "$b" && echo "删除分支: $b"
done
 
echo "==> 完成。当前 worktree 列表:"
git -C "$REPO" worktree list

作者复盘记录 · 2026-07-01 · 关联 herdr-终端AI Agent多路复用器深度调研