Summary
herdr lets you label an individual pane (rename-pane; shown on the pane border when show_agent_labels_on_pane_borders is on), but a herdr-plus project / worktree layout can't set those labels — ProjectPane carries only command and split. So codifying a hand-built workspace as a layout drops the per-pane names it had.
Add an optional label field to [[tabs.panes]]. When set, herdr-plus renames the pane right after it's created:
[[tabs.panes]]
label = "Server"
command = "make run"
Motivation
Concrete case — an app.harbor.my project whose server tab is three side-by-side service panes. Built by hand, they're labeled Server | Minio | OCR on their borders:
| pane label |
command |
| Server |
make run |
| Minio |
make minio |
| OCR |
make ocr |
Codify that workspace as a project template today and the three panes come up unlabeled — the tab is still named server, but the per-pane names are gone. Tabs have name; panes have no equivalent. This is the missing symmetric field.
Current workaround — and why it isn't enough
A pane's startup command can rename itself, since every pane's shell has $HERDR_PANE_ID:
[[tabs.panes]]
command = "herdr pane rename \"$HERDR_PANE_ID\" Server; make run"
Verified working, but it's a hack with real downsides:
- Can't label command-less panes. The trick needs a command to piggyback on. Layouts with empty panes (e.g. the empty
server panes in the remote-sqlite-osx / example.toml style) have nothing to attach to — those panes simply cannot be labeled.
- Pollutes the displayed command. herdr shows the running command on the pane; it becomes
herdr pane rename "$HERDR_PANE_ID" Server; make run instead of a clean make run.
- Shell-quoting fragility. A label containing a space, quote, or
; has to be escaped correctly inside a TOML string inside a shell command.
- Imperative, not declarative. A label is metadata about the pane, not a command to run — it belongs in a field, not smuggled into the command string.
Proposed solution
Add Label to ProjectPane and rename the pane immediately after it's created in the layout loop. It's a ~15-line change in code that already has everything it needs.
1. project.go — add the field
// ProjectPane is one pane within a tab. Command, when set, runs in the pane on
// startup. Split is how the pane is created relative to the previous pane.
// Label, when set, renames the pane after it is created.
type ProjectPane struct {
Command string `toml:"command"`
Split string `toml:"split"`
Label string `toml:"label"`
}
effectivePanes() already copies whole ProjectPane values, so the label flows through untouched — no change needed there.
2. herdr.go — add a paneRename client method
The pane.rename socket method already exists (CLI surface: herdr pane rename <pane_id> <label>|--clear). Mirror the existing tabRename (herdr.go:393):
// paneRename sets a pane's human label — the name herdr shows on the pane border
// (when show_agent_labels_on_pane_borders is on) and in pane lists.
func (c *herdrClient) paneRename(paneID, label string) error {
return c.call("pane.rename", map[string]any{
"pane_id": paneID,
"label": label,
}, nil)
}
(Param names mirror tab.rename's {tab_id, label}; confirm {pane_id, label} against herdr's socket schema when implementing.)
3. projects.go — rename in the layout loop
In layoutTabs (projects.go:108) the pane id is already in hand for both the root pane and each split (projects.go:131–143). Set the label as soon as the pane exists — it's instant metadata and doesn't need the prompt-pacing of the deferred command pass. strings is already imported.
for j, pane := range t.effectivePanes() {
paneID := tabRoot
if j > 0 {
paneID, err = client.paneSplit(prev, pane.Split, false)
if err != nil {
return fmt.Errorf("split pane %d in tab %q: %w", j+1, t.Name, err)
}
}
if lbl := strings.TrimSpace(pane.Label); lbl != "" {
if err = client.paneRename(paneID, lbl); err != nil {
return fmt.Errorf("label pane %d in tab %q: %w", j+1, t.Name, err)
}
}
if strings.TrimSpace(pane.Command) != "" {
runs = append(runs, pendingRun{pane: paneID, command: pane.Command})
}
prev = paneID
}
4. Worktree layouts — free
worktree.go builds through the same ProjectTab / effectivePanes / layoutTabs path, so worktree layouts pick up pane labels with no extra change.
5. Docs
examples/projects/example.toml — show label on a [[tabs.panes]] entry.
README.md "Split panes within a tab" (README.md:106) — document the field.
Design notes / open questions
- Scope to
[[tabs.panes]]. Single-command tabs (the command = shorthand, no [[tabs.panes]]) already take their name from the tab's name, so a pane label there is redundant; only multi-pane tabs need it.
- Blank label = no-op. A missing/blank
label skips the rename — no implicit clear of the pane's default name.
- No new validation. Any string is a valid label; just trim it.
validateTabs needs no new rule.
- Failure handling. The sketch treats a failed rename as fatal, matching the surrounding split/command errors. If a label is considered purely cosmetic, downgrade to best-effort instead. I lean fatal, to surface socket problems early.
Acceptance criteria
Notes
pane.rename is confirmed present in herdr core; the CLI surface is herdr pane rename <pane_id> <label>|--clear.
- Sizing: small / good-first-issue — one new field, one client method, one call site, all with existing
tabRename / effectivePanes patterns to copy.
Summary
herdr lets you label an individual pane (rename-pane; shown on the pane border when
show_agent_labels_on_pane_bordersis on), but a herdr-plus project / worktree layout can't set those labels —ProjectPanecarries onlycommandandsplit. So codifying a hand-built workspace as a layout drops the per-pane names it had.Add an optional
labelfield to[[tabs.panes]]. When set, herdr-plus renames the pane right after it's created:Motivation
Concrete case — an
app.harbor.myproject whoseservertab is three side-by-side service panes. Built by hand, they're labeled Server | Minio | OCR on their borders:make runmake miniomake ocrCodify that workspace as a project template today and the three panes come up unlabeled — the tab is still named
server, but the per-pane names are gone. Tabs havename; panes have no equivalent. This is the missing symmetric field.Current workaround — and why it isn't enough
A pane's startup command can rename itself, since every pane's shell has
$HERDR_PANE_ID:Verified working, but it's a hack with real downsides:
serverpanes in theremote-sqlite-osx/example.tomlstyle) have nothing to attach to — those panes simply cannot be labeled.herdr pane rename "$HERDR_PANE_ID" Server; make runinstead of a cleanmake run.;has to be escaped correctly inside a TOML string inside a shell command.Proposed solution
Add
LabeltoProjectPaneand rename the pane immediately after it's created in the layout loop. It's a ~15-line change in code that already has everything it needs.1.
project.go— add the fieldeffectivePanes()already copies wholeProjectPanevalues, so the label flows through untouched — no change needed there.2.
herdr.go— add apaneRenameclient methodThe
pane.renamesocket method already exists (CLI surface:herdr pane rename <pane_id> <label>|--clear). Mirror the existingtabRename(herdr.go:393):(Param names mirror
tab.rename's{tab_id, label}; confirm{pane_id, label}against herdr's socket schema when implementing.)3.
projects.go— rename in the layout loopIn
layoutTabs(projects.go:108) the pane id is already in hand for both the root pane and each split (projects.go:131–143). Set the label as soon as the pane exists — it's instant metadata and doesn't need the prompt-pacing of the deferred command pass.stringsis already imported.4. Worktree layouts — free
worktree.gobuilds through the sameProjectTab/effectivePanes/layoutTabspath, so worktree layouts pick up pane labels with no extra change.5. Docs
examples/projects/example.toml— showlabelon a[[tabs.panes]]entry.README.md"Split panes within a tab" (README.md:106) — document the field.Design notes / open questions
[[tabs.panes]]. Single-command tabs (thecommand =shorthand, no[[tabs.panes]]) already take their name from the tab'sname, so a pane label there is redundant; only multi-pane tabs need it.labelskips the rename — no implicit clear of the pane's default name.validateTabsneeds no new rule.Acceptance criteria
ProjectPaneaccepts an optionallabel(TOML keylabel).labelis renamed to it on project open — root pane and split panes alike.label(shared path; add/extend aworktree_test.gocase if practical).labelleaves the pane's default name untouched.project_test.gocovers:labelparses from TOML and surviveseffectivePanes(mirrors the existingeffectivePanestests aroundproject_test.go:195/:204).examples/projects/example.tomlandREADME.mddocument the field.Notes
pane.renameis confirmed present in herdr core; the CLI surface isherdr pane rename <pane_id> <label>|--clear.tabRename/effectivePanespatterns to copy.