Names a tab running a coding agent after the task the agent reports -- Squash merge command instead of a fifth tab reading claude. herdr publishes the title on the pane object the reconcile already holds, so the label costs no extra herdr call. It follows the work on an event the plugin already subscribes to, so nothing polls for it.
A tab with more than one pane is named after the pane that matters. For the usual agent-and-shell split that is the agent, not the half you happen to be typing in. Several naming bugs go with it: a background split kept the name it had before the split, a tab whose rename herdr rejected was never named again, and a label carrying a backslash or a control character reached herdr mangled.
Upgrade note for anyone running a coding agent in a tab. AGENT_TITLES defaults to on, so those tabs stop reading claude and start reading the task. Set AGENT_TITLES=0 to keep the program name. MAX_TITLE_LEN (28) and TITLE_IGNORE tune what gets through.
Added
-
A tab running a coding agent is named after the task the agent reports rather than after the agent (
AGENT_TITLES, default on). Naming by foreground program has one case it cannot answer: fiveclaudetabs all readclaude, and the tab bar stops being a way to find anything. A coding agent already publishes the task as its terminal title and keeps it current as the work moves, so that is what the tab shows --Squash merge command,Check PR 2169 relevance. herdr publishes the title on the pane object the reconcile already holds, so reading it costs no extra herdr call on any version, and a tab named this way needs nopane process-infoat all, which leaves an agent-heavy session making fewer herdr round-trips than before. The label follows the work onpane.agent_status_changed, an event the plugin already subscribes to, so nothing polls for it.A title only replaces the program name when it says something.
ar_title_cleanrefuses the three kinds that say nothing: one naming the agent instead of its work (the agent's own herdr kind, that kind followed bycode, or aTITLE_IGNOREentry, which is what an agent titles a session it has no task for yet), one that is just the directory the pane sits in, and a bare number, which is herdr's own generated tab label handed back through the title. Every refusal falls through to the program name,PROGRAM_ALIASESand theWRAPPER_PROGRAMSunwrapping included. A path to that directory is refused the same way, since an agent titling itself~/dev/apiis still naming where it sits: only a title with no whitespace, though, because a description likeFix build for services/paymentsalso ends in a directory name and is still the work. Every comparison is made against the title in the shape the tab would carry it, so a trailing space cannot walk a refused title past them.A title that survives replaces the program name outright, alias and all:
AGENT_TITLESis the request for the task, and an alias shorteningclaudetoclis not a request to hide the work. The program still supplies the icon, and the label gets its own budget,MAX_TITLE_LEN(28) rather thanMAX_NAME_LEN(20), cut at a word boundary when that leaves at least half of it: a title is a sentence, and 20 characters were sized for command names.Before any of that, the leading run of non-alphanumerics comes off. herdr keeps an ANSI-stripped copy of the title and stripped means exactly that: an agent parks a spinner glyph in front of its title while it works, and claude cycles four of them. Without the strip the label would flip between
Taskand<glyph> Taskon every status event, and each flip is a rename.jqdoes that strip, and the case-folding for the comparisons, in one call, because its character classes know Unicode: a byte-wise strip under the C locale herdr may launch a plugin in would eat the first letter of a title that opens with a non-ASCII word. The function still takes strings and returns strings, which is what keeps it innaming.sh. -
A tab with more than one pane is named after the pane that matters. Naming it from its own focused pane, which is what the layouts fix below gives it, still read the wrong half of the usual agent split. An agent sits in one pane and a shell in the other, and focus is in the shell most of the time, so the tab advertised
zshwhile the interesting thing sat beside it. The pick is now, in order: the tab's own focused pane when herdr reports an agent in it, then any pane of the tab holding an agent that is working or blocked, then the focused pane. So a split stays about the agent while you read or type in the shell half, and an idle agent stops at the second rule on purpose -- a finished agent should not hold the name of a tab you have moved on to. The choice is made in the snapshot reshape and travels on the tab row as_name_pane, so the tab loop costs nothing extra for it. A herdr whose snapshot carries no layouts can answer neither the first rule nor the third, since both ask for the tab's own focused pane. The second asks only for an agent at work, so it still answers there, and everything it does not answer falls to the older inference (the sole pane of a single-pane tab, else the tab's own focused pane). -
The
resetandclearactions report what they did as a herdr notification. Both are built for a keybinding, where the only feedback was the tab bar redrawing. Aresetthat found no tab to re-adopt, or that ran underNAME_TABS=0, looked exactly like one that worked, andclearon an already clean session looked like nothing at all. Each outcome now names itself. A re-adoption is reported only when both halves happened: the tab had opted out (read before its state is cleared) and it is named and owned again by the end of the pass. Reporting it from the first half alone told the user naming was back on even when the rename that followed failed, and a tab in that position opts itself straight back out on the next event.Both actions also wait for the lock instead of deferring to whoever holds it. Events can defer, because any pass computes the same names, but an action carries a request that lives in its own process, so handing the job over dropped it: a reset pressed during a burst of events did nothing at all, and reported nothing either, since the deferral exited before the notification. Best effort, so a herdr without
notification showdeclines it and the action still does its work; the uninstall path (--clear) notifies through the same helper.
Fixed
-
Running the test suite inside a herdr pane renamed that session's tabs. The hook tests source the real hooks, and a hook resolves the real engine next to itself, so with
HERDR_TAB_IDinherited from the pane and nothing pointing the engine at the fake herdr, every hook call the tests made went to the live session. They run fully sandboxed now. -
A tab whose rename herdr rejected stopped being named at all. Ownership was recorded before the rename was issued, so a failed one left state claiming a base the tab did not carry; the next pass compared the two, read the mismatch as a name typed by hand, and opted the tab out of auto-naming for good -- recoverable only through the
resetaction. Ownership is now recorded for a name the tab actually carries: the label already matches, or the rename reported success. The shell-hook fast path has always ordered it this way. -
A background tab with more than one pane kept the name it had when it last had a single pane, so a split that started an agent still read
nvim. Naming had no pane to read: none of a background tab's panes carries.focused, and the pane list is all the pass looked at. It now takes the tab's ownfocused_pane_idfrom the snapshot'slayouts, which answers for every tab. Older herdr, whose snapshot has no layouts, keeps the previous behavior -- except for a tab with an agent at work in it, which the rule above names without asking for a layout. -
The focused tab was named from the GLOBALLY focused pane, which belongs to whichever client moved focus last. With a second client attached, or a remote attach, that pane can sit in a different tab. Per-tab layouts remove the guess, and the pane-list fallback now reads only the tab's own panes. A focused tab with several panes and no focused pane of its own -- which is what that same second client looks like on a herdr with no layouts -- keeps the label it has rather than being named after an arbitrary one of them.
-
Where herdr named no foreground process group at all but still reported one process for the pane, no name was computed. Some Linux container and sandbox setups cannot expose a foreground group, which is what left tab naming doing nothing there. A single reported process is not a choice, so such a pane is now named after it. Two or more with no named group would be: herdr does not order that list and documents its own degraded detection as one where a background job can look like the foreground one, so those panes keep the label they have. A group herdr DID name whose process is absent from the list is a group racing its own exit, and still computes no name; so does a pane herdr reports no process for.
-
A label carrying a control character reached herdr verbatim:
argvcan hold a newline or a tab, soSHOW_PROGRAM_ARGS=1could put one in the tab bar. They are now replaced where they arrive, in the jq that readspane process-info, and the label has its whitespace runs collapsed and its ends trimmed before truncation. A clean label -- nearly all of them -- costs no extra process.The two values that reader returns travel one per line now rather than as TSV.
@tsvescaped a real tab into the printable two characters\t, which no scrub downstream can tell from text somebody typed, and it doubled every backslash in a command line on the way past. -
A tab carrying no label at all was never named again. Its row had an empty field in the middle, and the rows were split on a tab, which bash counts as whitespace:
readcollapses a run of them, so every field after the empty one shifted and the tab read its own pane count as its label.HIDE_SHELL=1with numbering off is exactly that state, so a tab blanked once stayed blank however many programs ran in it. Rows are split on the ASCII unit separator now, which keeps an empty field in place; nothing can carry one, since the values have their control characters removed on the way out of jq. -
A name this plugin owns is now written until herdr holds exactly it. Rows arrive with control characters replaced, so a label carrying one read as equal to the name computed for it and no rename looked necessary, which left the character there for good. A label the plugin does NOT own is still left exactly as the user typed it.
-
Numbering a tab whose name holds a backslash rewrote that name. Every row the reconcile reads came back through
@tsv, which doubles a backslash, so a tab calledC:\tempwas renamed toC:\\temp(workspace and agent rows read the same way). Rows now carry their values unescaped and drop control characters instead, which is what keeps them parseable. The same change lets a tab whose label carries a control character stay owned: escaped, it matched nothing this plugin had recorded and the tab read as renamed by hand.
Full Changelog: v0.6.1...v0.7.0