Skip to content

Add a label field to [[tabs.panes]] so layouts can name panes declaratively #26

Description

@cloudmanic

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 labelsProjectPane 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

  • ProjectPane accepts an optional label (TOML key label).
  • A pane with a label is renamed to it on project open — root pane and split panes alike.
  • Works for panes with no command (closes the workaround's gap).
  • Worktree layouts honor label (shared path; add/extend a worktree_test.go case if practical).
  • A blank/omitted label leaves the pane's default name untouched.
  • project_test.go covers: label parses from TOML and survives effectivePanes (mirrors the existing effectivePanes tests around project_test.go:195/:204).
  • examples/projects/example.toml and README.md document the field.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions