gitea-adminandClaude Fable 5.1 72d594033e Stop cancelled remote queries without blocking the selector
RemoteQueryPool.cancel() terminated the SSH process and then waited up
to 250 ms for it, on the curses thread. Ctrl+R re-enqueues every host,
so a refresh of six hosts could freeze the popup for over a second.

Only send SIGTERM when cancelling; reap the process on later polls,
escalating to SIGKILL after 250 ms and closing its pipes once it has
exited. close() still waits, since the popup is exiting anyway.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 03:04:27 +00:00

herdr-hub

herdr-hub 是一个 Linux 下的 Herdr session 切换器。它让 tmux 做最外层路由, 把每个本地或 herdr --remote attach 放进独立 tmux window,并在 Herdr 画面顶部 显示可点击的 Sessions 条。

它不是 Herdr 插件,也不会修改 Herdr core。Herdr 0.8.2 的插件 API 不能向原生 sidebar 插入 UI,也不能让 thin client 从一个 server 原地改连另一个 server; 所以顶部状态栏横跨整个终端,而不是只嵌在 Spaces 上方。

功能

  • 自动读取 herdr session list --json,导入本机正在运行的 sessions。
  • 记住使用过的远端,并通过 SSH 实时读取运行中及已停止的 Herdr sessions。
  • 远端查询兼容 Windows OpenSSH 与 Linux/macOS shell;Windows target 自动改用并持久记住 SSH TTY 模式。
  • 在 Linux /proc/*/cmdline 中查找仍在运行的 herdr --remote … clients,并自动记住目标。
  • 首次接管已有 remote client 时安全地再 attach,不结束原进程。
  • 一个 endpoint 对应一个 tmux window,隐藏 window 中的连接继续在线。
  • 顶部 Sessions 条在窄屏下也为每个 Tab 显示主机优先的短名称,并支持鼠标点击、数字切换和无需 fzf 的动态 popup 选择。
  • 可隐藏不再需要的 Tab,而不停止底层本地或远端 Herdr Session。
  • 可在 Sessions 管理弹窗中为 Tab 设置持久化显示名称,或恢复默认名称。
  • dead window 留在顶栏,可原地重连。
  • 不修改 ~/.tmux.conf,不保存 SSH 凭据或远端 Session 缓存。

依赖

  • Linux
  • Python 3.11+
  • tmux 3.4+
  • Herdr 0.9+(本地和需要 Fork 的远端目标均需支持 0.9 的控制 API)
  • OpenSSH client

检查当前环境:

./bin/herdr-hub doctor

安装

直接从仓库运行:

./bin/herdr-hub

或把启动器链接到 ~/.local/bin:

make install
herdr-hub doctor

安装脚本只创建一个指向本仓库的符号链接。移动或删除仓库后需重新安装。

第一次接管

如果本地和 remote Herdr clients 已经分别开在其他终端中:

  1. 保持这些 clients 运行。
  2. 从一个新的、未嵌套在 tmux 中的终端执行 herdr-hub。
  3. Hub 扫描现有 clients,并为每个 endpoint 创建一个新的 attach window。
  4. 确认 Hub 中可以正常操作后,手动退出原来的 clients。

tmux 不能把另一个终端里已经运行的进程搬进 window,所以首次接管会短暂存在两个 clients;它们连接的是同一个 Herdr server,不会复制 server 或 panes。

只有一个终端时,本地 Herdr 可以先 detach,因为 herdr session list 仍能发现后台 server。remote target 在 client 退出后没有本地记录,因此直接使用:

herdr-hub open --remote orangepi
herdr-hub open --remote orangepi --session work
herdr-hub open --remote ssh://me@server:2222 --remote-keybindings server
herdr-hub open --remote windows-host --transport ssh

打开本地 named session:

herdr-hub open --session work

open 会把 endpoint 保存在当前 tmux hub 的 window metadata 中。远端 target 还会 同时加入 ~/.config/herdr-hub/remotes.json,以后可直接从 Sessions 弹窗浏览这台机器上的其他 Session。

日常操作

Hub 使用独立 tmux socket herdr-hub,不会接管普通 tmux server。外层前缀固定为 Ctrl+A,Herdr 原来的 Ctrl+B 可以直接使用。

操作 快捷键 / 命令
打开快捷键帮助 Ctrl+A ?
点击切换 鼠标点击顶部 session
打开 Herdr 命令菜单 Ctrl+A h
打开 Session 弹窗 点击左上角 Sessions 或 Ctrl+A g
上一个/下一个 window Ctrl+A p / Ctrl+A n
按编号切换 Ctrl+A 0..9
扫描新增 sessions Ctrl+A r 或 herdr-hub refresh
强制重连当前 window Ctrl+A R
关闭/隐藏当前 Tab Ctrl+A q,确认 y;也可在 Sessions 弹窗中选择
关闭并停止远端 Session Sessions 弹窗 → 选中 Session → Space / [⋯] → 关闭并停止 Session
修改 Tab 显示名称 Sessions 弹窗 → 选中 Session → Space / [⋯] → 修改显示名称
detach Hub Ctrl+A d
重新进入 Hub herdr-hub
停止 Hub clients herdr-hub stop

Fork 当前 Agent 到新 Tab

按 Ctrl+A h 打开独立的 Herdr 命令菜单,选择“Fork 当前会话到新 Tab”, 按 Enter 或单击执行,Esc 关闭。菜单显示当前 Agent、Workspace、Fork 和更新操作及必要的状态信息。 操作正在执行时需等待结果后再关闭,以免将关闭弹窗误认为取消已经发送的请求。 Sessions 弹窗仍使用 Ctrl+A g;两个菜单都使用 Hub 外层的 Ctrl+A 前缀。 快捷键集中定义在 herdr_hub/control.py 的 CONTROL_KEY 常量中。

Fork 复制的是 Codex / Claude conversation,并保留正在运行的原 Agent 和原 Tab。 它在同一个 Herdr Workspace、相同 cwd 新建 Tab,使用当前 pane 已报告的 conversation ID: Codex 执行 codex fork UUID;Claude 执行 claude --resume SESSION_ID --fork-session。 新 Agent 被 Herdr 确认为就绪后才切换焦点。它不会新建外层 Herdr named session, 也不会创建 Git worktree;两个 Agent 会编辑同一工作目录,需自行协调并发修改。

支持本地 default / named session,以及通过 SSH 控制的远端 native / Windows SSH endpoint。 控制目标取自触发菜单的 Hub window metadata,不使用 popup 继承的 Herdr socket 或 pane 环境变量。 目标必须运行 Herdr 0.9+,并安装对应 Agent;当前 pane 必须已报告可用的 Agent session ID。 Fork 显示灰色时,菜单会说明原因;按 Ctrl+R 可重新读取当前 Agent 信息。 若提示尚未上报会话 ID,在该主机检查 herdr integration status,确认对应 Agent 的集成已加载。 识别出 Codex / Claude 并不代表已经取得 conversation ID;Hub 不会根据目录猜测会话。 远端控制使用非交互 SSH(需要已信任的主机密钥和可用认证);Windows 需要 PowerShell。 启动 blocked、失败、超时或响应不完整时,菜单保留错误信息,不会自动关闭任何 Tab。 超时并不代表远端没有创建或启动成功,请先检查提示中的新 Tab,再重新打开菜单重试。

CLI 入口是 herdr-hub control。弹窗打开时会锁定 Hub 当前激活的 managed window, 再从其 Endpoint 对应的 Herdr server 读取当前页面的 Agent conversation。 更新后运行 herdr-hub refresh 配置现有 Hub 的快捷键。herdr-hub doctor 会显示入口和版本要求。

从命令菜单更新 Herdr

需要让运行中的服务器也使用新版本时,按 Ctrl+A h → 更新并热重启 Herdr。 此操作先更新当前 Tab 所在主机的 Herdr,再对当前 Session 执行实验性的 live handoff, 尝试保留 Agent、shell 和其他 pane 进程。它会核对服务器版本;确认使用更新后的版本后, 自动关闭菜单并重新连接打开菜单时的那个 Hub Tab。即使此前已安装新版本、服务器仍是旧版, 也会尝试热切换。该 Session 内的所有 Workspace/Tab 共用服务器,因此都会随之切换。

更新阶段使用 herdr --session NAME update --handoff,兼容 0.8.x 在非交互 SSH 下的更新要求。 原生更新已经尝试热切换却未切换成功时,不会再次发送请求;仅在安装文件未变化、旧服务器仍运行时补做热切换。

热切换适用于支持该能力的 Linux/macOS Herdr 服务器;远端还需有 python3。 Windows 不支持此入口,可以使用下面的普通更新。失败、超时、服务器不支持或未确认新版本时, 菜单保留诊断,不自动执行普通重启,也不重试热切换。连接中断时操作可能已经完成,请先检查 目标 Session。普通重启会结束 pane 进程;缺少会话 ID 的 Agent 不能保证自动恢复。

按 Ctrl+A h,选择“更新 Herdr”,按 Enter 或单击执行。更新目标跟随打开菜单时的 当前 Hub Tab:本地 Session 更新本机,远端 Session 通过 SSH 更新对应主机(包括 Windows)。 菜单会显示目标主机;更新作用于该主机的 Herdr 安装,而非单个 Session。当前 pane 没有 Agent 或无法读取 Agent 信息时,仍可使用更新操作。

该操作执行 herdr update,沿用目标主机已有的更新渠道,不传入 --handoff。 它适用于 Herdr 自带安装器管理的安装;Homebrew、mise、Nix 等安装需使用对应包管理器更新。 这个普通更新入口不自动重启 Session 或重新连接客户端。完成后可关闭菜单,按 Ctrl+A R 重新连接; 服务器端新功能可能仍需另行重启服务器。兼容的服务器会继续运行;旧版本升级若要求停止 Session 或交互确认,本菜单不会代为确认,请根据命令输出在终端处理。

更新在后台执行,实时显示命令输出,最长等待 5 分钟。小窗口自动使用紧凑布局,保证结果区可见。 下载器没有新输出时,菜单显示已执行时间和无新输出时间。 读取 Agent 信息期间也可选择更新。结果留在菜单内,可用 Page Up/Down 翻阅,Esc 关闭。 执行期间需等待结果;同一次菜单内更新只能执行一次,失败或超时也不会自动重试。 超时或 SSH 断开不代表目标主机已取消更新,请检查目标状态后再重新打开菜单。

远端更新使用远端自己的代理。优先保留 SSH 进程已有的 http_proxy、https_proxy、 all_proxy、no_proxy(兼容大写);Linux/macOS 还通过远端 python3 读取 /etc/environment、/etc/profile、/etc/profile.d/*.sh、~/.config/environment.d/*.conf 及用户 profile、bash/zsh 配置中的独立、无缩进的字面量赋值或 export 行。 不会执行整个 shell 配置文件,也不解析命令替换、变量引用或条件表达式。 没有上述代理变量时,还会读取远端 Clash Verge、Mihomo/Clash 常见运行配置中的 mixed-port、port 或 socks-port,确认对应回环端口可连接后用于本次更新。 Linux 下会优先从正在运行的 Mihomo/Clash 进程读取 -f / -d 指定的配置路径,支持 systemd 服务使用自定义目录。 这覆盖了 TUN 模式下没有环境变量的情况,不会修改桌面系统代理开关。 需要认证的应用代理、不在已支持路径中的配置及未运行的端口不会自动采用。 Windows 普通更新会补读远端用户和系统的代理环境变量。 检测结果只显示配置来源,不显示代理地址或凭据;配置仅传给本次更新进程。 没有检测到代理时沿用远端网络和 curl 自身配置,不自动借用本机代理。 “未检测到代理”只是网络方式说明;更新失败时,菜单会先显示末尾的具体错误,再列完整输出。

远端 Session 弹窗

弹窗由 tmux display-popup 和 Hub 内置选择器组成,不依赖 fzf。它会立即显示已打开的 window,同时在后台并行查询所有已登记远端;每台机器的结果就绪后原地更新。

主页面只显示 Session 和两个全局入口。大多数主机只有 default Session,因此默认只显示 主机名;有命名 Session 时显示为 主机 / Session。本机同样显示为“本机”或 “本机 / Session”。状态与主操作如下:

状态 选择后的行为
▶ 当前 / ● 已打开 跳转到已有 window
× 已断线 自动重连并跳转
○ 远端运行 新建 attach window 并跳转
■ 远端已停止 Enter/单击启动并打开
↻ Windows 旧窗口 原地切换为 SSH TTY 模式并重连

Enter 或单击一行始终执行它的主操作。管理操作统一放在上下文菜单中:键盘选中一行后按 Space,或在手机端点击行尾 [⋯];右上角 [操作] 也会打开当前行的菜单。菜单会按 Session 当前状态提供修改显示名称、关闭 Hub Tab、关闭并停止 Session、永久删除已停止的 命名 Session、在此远端新建 Session、重试连接和移除远端记录等可用操作。Esc 逐层返回; 危险操作会二次确认并默认选中“取消”。关闭 Hub Tab 只结束该 Tab 中的 client,底层 Session 和工作现场保持运行;“关闭并停止”则会中断远端任务。若它是 Hub 中最后一个 Tab, 该 Tab 会保留为断线状态,避免销毁承载弹窗的最后一个 window。

主页面只有一个“新建 Session”入口,选择远端后再输入 Session 名;“添加远端”会先让你 选择“自动检测(Linux/macOS)”或“Windows/SSH”。修改显示名称支持最多 48 个字符,留空 保存可恢复默认名称,且不会修改底层 Session ID。移除远端记录只删除本机记忆,不关闭已有 Tab,也不停止远端 Herdr server。

搜索必须按 / 显式进入,因此普通字母不会误触命令;搜索页输入文字即可筛选,Esc 清空并 返回。键盘还支持上下键、Page Up/Down、Enter、Ctrl+R 刷新和 Esc 返回/关闭。窄屏会优先 显示主机和 Session 名,状态详情可在 [⋯] 菜单中查看。

永久删除只会出现在已停止的命名 Session 的菜单中,无论是否仍有关联 Hub Tab 都可执行。 确认后会在后台通过 SSH 执行 herdr session delete,永久删除工作现场和历史数据;关联 Tab 也会一并关闭。运行中的 Session 需先“关闭并停止”,Herdr 永久保留的 default Session 不能删除。

Herdr 0.8.2 不支持 Windows 作为原生 herdr --remote 的目标主机。Hub 查询到 Windows Session 后会标记为“Windows · SSH 模式”,并自动改为 ssh -tt target herdr;命名 Session 会在远端运行 herdr --session NAME。这种模式仍支持 Tab 切换、detach、断线 重连和远端工作现场持久化,但使用远端 Herdr 的 keybindings,也没有原生 thin client 提供的本地图片剪贴板桥接。空 Windows 主机或平台检测失败时,可在“添加远端”中选择 “Windows / SSH”,也可手动执行 herdr-hub open --remote TARGET --transport ssh。

rootless 容器或 UID 映射环境可能把 /etc/ssh/ssh_config 显示为 nobody 所有,导致原生 herdr --remote 报 Bad owner or permissions,即使系统 ssh 本身仍可使用。不要放宽该 文件的写权限;可在本机 ~/.config/herdr/config.toml 中让 Herdr 直接使用系统 SSH:

[remote]
manage_ssh_config = false

这会停用 Herdr 自动生成的 keepalive 和连接复用配置,但不会禁用用户自己的 SSH 配置。 执行 herdr config check 验证后重新打开 Session 即可。若 Session 进程启动后立即退出, Sessions 弹窗会保留在该 Session 的操作页,显示 pane 返回的错误并允许直接重试。

也可通过 CLI 管理远端:

herdr-hub remote list
herdr-hub remote add orangepi
herdr-hub remote add ssh://me@server:2222 --remote-keybindings server
herdr-hub remote add windows-host --transport ssh
herdr-hub remote remove orangepi

注册表默认位于 ~/.config/herdr-hub/remotes.json(遵循 $XDG_CONFIG_HOME),权限为 0600。它只保存 SSH target、remote keybinding 模式和首选 transport,不保存凭据。

Session 显示名称

Herdr 0.8.2 没有底层 Session rename 命令,因此 Hub 的“重命名”是安全的显示名称: 顶部 Tab 和 Sessions 弹窗显示新名称,attach、重连和工作现场仍绑定原来的 Session ID。 名称会跨终端断线、Hub detach 和 herdr-hub stop 保留,并在重新打开 endpoint 时自动 应用。配置保存在 ~/.config/herdr-hub/aliases.json(遵循 $XDG_CONFIG_HOME), 权限为 0600;留空保存会删除该 endpoint 的显示名称并恢复 local/work、 host/work 等默认格式。

复制与剪贴板

Hub 启用 tmux 的 OSC 52 剪贴板透传。Herdr 触发的自动复制,以及 tmux 接管后的 鼠标拖选,都会尝试写入当前 Hub 访问端的本地剪贴板。远程 session 中复制的 文本也回到访问端,不是远程机器的剪贴板。

访问端的终端需要支持并允许 OSC 52。如果终端或 Web terminal 禁止了这项能力, 可按住 Shift 再拖选,绕过 tmux 并使用终端原生复制。

Termux / 手机端复制

Termux 的原生长按选区只认识整个终端画面,不知道 Herdr pane 和左侧 sidebar 的边界。 跨行拖选时,中间行因此可能混入 sidebar 文本。优先使用 Herdr 自带的 Copy Mode:按 Ctrl+B [ 进入,用方向键或 h/j/k/l 移动,按 v 或空格开始选择,再按 y 或 Enter 复制。它只读取当前 pane,不会包含 sidebar;复制大段历史输出也可按 Ctrl+B e 在编辑器 中打开当前 pane 的 scrollback。这里的 Ctrl+B 是 Herdr 前缀,Hub 外层 tmux 前缀仍为 Ctrl+A。

如果更习惯 Termux 长按拖选,可在 ~/.config/herdr/config.toml 已有的 [ui] 段中加入:

[ui]
pane_scrollbars = false
sidebar_collapsed_mode = "hidden"
# sidebar_start_collapsed = true  # 可选:下次启动时默认收起

不要重复创建 [ui] 段。执行 herdr config check 和 herdr server reload-config 后,按 Ctrl+B b 可在完整 sidebar 和零宽度隐藏之间切换;隐藏后再使用 Termux 原生选区即可。 pane_scrollbars = false 还会移除 pane 右侧滚动条并腾出一列空间。

当访问端宽度小于 80 列时,Hub 会自动进入窄屏模式: 所有 Tab 都显示状态和最多 12 列的短名称,并优先显示主机:默认 Session 的本地与 远端分别显示 local、host,非默认 Session 才显示 local/work、host/work。 顶部右侧只保留 C-a ? 帮助;宽度小于 50 列时缩成 C-a ?。左上角标题也会缩成 S,继续给 Tab 留空间。点击 Sessions/S 可打开占 90% 屏幕的完整选择器。

refresh 是 add-only:它添加新发现的 endpoint,不自动删除已有 window,也不会改变 当前焦点。通过 Ctrl+A q 或弹窗关闭的 endpoint 会在当前 Hub 生命周期内记为已隐藏, 刷新和断开终端后重新进入都不会让它自动回来。要恢复它,打开 Sessions 弹窗并重新选择; 也可以执行对应的 herdr-hub open 命令。远端通过 SSH 目录重新选择,本地隐藏的运行中 Session 会列在“本机”下。Hub 至少保留一个 Tab;要全部退出请使用 herdr-hub stop。

当 endpoint 连接进程退出时,window 会保留:顶栏将它标记为 × [断开], 当前 pane 底部会显示退出状态和 Ctrl+A R 重连提示。这与正常 detach Hub 不同;仅关闭外层终端后,直接再次执行 herdr-hub 即可。

herdr-hub stop 只结束 tmux 中的 client processes。本地及远端 Herdr servers 和 它们的 panes 会继续运行。

CLI

herdr-hub
herdr-hub start
herdr-hub refresh
herdr-hub open [--session NAME]
herdr-hub open --remote TARGET [--session NAME]
               [--remote-keybindings local|server] [--transport native|ssh]
herdr-hub choose
herdr-hub remote list
herdr-hub remote add TARGET [--remote-keybindings local|server]
                            [--transport native|ssh]
herdr-hub remote remove TARGET
herdr-hub reconnect [--window ID]
herdr-hub close [--window ID]
herdr-hub doctor
herdr-hub stop

自动发现与远端查询的边界

零配置的首次发现只覆盖:

  • 当前用户正在运行的本地 Herdr servers;
  • 当前用户、同一 PID namespace 中仍然存活的 herdr --remote processes。

发现或手动添加一次后,remote target 会被记住,不再要求本地 client 持续存活。弹窗先 通过非交互 SSH 执行跨平台的 herdr session list --json;Linux/macOS 的非交互 PATH 找不到 Herdr 时,再自动尝试 $HOME/.local/bin/herdr。Windows OpenSSH 返回的本地代码页 错误文本也会被容错解码。SSH key/passphrase 应已由 ssh-agent 处理;未知 host key、需要 密码、远端不可达或命令失败时,该主机显示“查询失败 · 单击重试”,不会关闭或卡住整个 弹窗。首次安装或 host-key 确认仍可在实际新建的 Herdr window 中交互完成。

首次使用且尚未登记时,以下情况仍无法自动找回 remote target:

  • remote client 已经退出,但远端 server 仍在后台运行;
  • client 位于另一个 container/PID namespace;
  • /proc 使用了阻止读取同用户 argv 的安全配置;
  • 可执行文件不叫 herdr。

这些场景可在弹窗中选择“添加远端”,或使用 herdr-hub open --remote …。为了避免嵌套 tmux 与前缀冲突, herdr-hub 应从普通终端启动;若当前已经在另一个 tmux server 中,它会拒绝再次 attach。

开发与测试

make test

测试使用伪造的 /proc 和 Herdr executable;tmux 集成测试使用唯一 socket 和 /dev/null 配置,不接触真实 Herdr sessions 或现有 tmux server。

S
Description
Responsive local and remote Herdr session switcher built on tmux
Readme
168 KiB
Languages
Python 99.9%
Shell 0.1%