configure() ran one tmux client per option and per key binding, and
refresh_labels() one or two per window, on every start/refresh/open: a
no-op refresh with three windows spawned 45 tmux processes. On a phone
each spawn is noticeable.
Add run_batch(), which joins commands with ';' separators (escaping
arguments that end in ';', which tmux would otherwise split) and replays
them one by one if the batch fails so errors stay attributable. Use it
for configure(), window creation, endpoint replacement and label
refreshes. list-windows now also reports the stored short label so
unchanged labels are skipped, and managed_windows() no longer probes
has-session first. The same no-op refresh now spawns 9 processes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first ping after `herdr update --handoff` was sent exactly once. A
server still binding its socket turned into "更新/热切换未完成" even
though the update had succeeded. Retry connection errors for up to 20
seconds before giving up; a healthy server still answers immediately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The update popup kept the last 64 KiB of raw bytes and decoded the
whole buffer on every refresh. Cutting the buffer, or reading a chunk
that ended inside a multi-byte sequence, made the UTF-8 decode fail and
sent everything through the GB18030 fallback, garbling the display.
Clip the retained tail at a UTF-8 boundary and decode still-growing
streams with an incremental decoder that holds back an incomplete
trailing sequence instead of treating it as corruption.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Display names starting with '-' were parsed by tmux rename-window as
flags. The alias was persisted before tmux rejected it, so every later
start/refresh/open failed in refresh_labels until aliases.json was
edited by hand. Guard rename-window with '--', apply the name before
persisting it, and roll back the in-memory table on failure.
- Start the Hub tmux server with -f /dev/null by default. Reading the
user's ~/.tmux.conf let theme status-formats replace the Sessions bar
and root-table bindings leak into Herdr; HERDR_HUB_TMUX_CONFIG still
opts into a custom file.
- An unreadable aliases.json blocked start/refresh/open/choose. Degrade
to default names with a warning (stderr, or the popup message) and
leave the file untouched for `doctor` to report.
- `doctor` crashed with a traceback when a *_BIN override pointed at a
missing executable; report it as a failure instead.
- Refuse to run inside another tmux server before discovery and server
creation, so no unattached Hub is left behind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Opening a window whose target was removed from the registry (its tab
stays open by design) and failing landed in the action menu with a
"重试连接" row that indexed self.entries directly and raised KeyError,
killing the popup with a traceback.
Only offer the retry action for registered targets, degrade a stale
retry row to a message, and keep the popup alive when a handler raises
anything unexpected instead of dying with a traceback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Ctrl+A h command menu gains two host-scoped actions: a plain
`herdr update` that streams updater output into the popup, and an
experimental update-and-hot-restart flow that verifies a live server
handoff and reconnects the originating Hub tab once the popup closes.
Remote updates use the remote host's own proxy settings. A standalone
helper reads literal proxy assignments from shell profiles and running
Clash/Mihomo listeners without executing startup files; Windows reads
process, user and machine proxy variables through PowerShell.