Herdr Workspace Tab Pane Architecture: Immutable Three-Layer Design

Herdr organizes its terminal multiplexer UI into three immutable, testable layers—Workspace containers, Tab-based pane trees, and pure BSP (Binary Space Partitioning) layout engines—that strictly separate state management from terminal runtime operations.

The ogulcancelik/herdr repository implements a Rust-based terminal workspace manager where every UI mutation flows through a predictable hierarchy. Understanding the herdr workspace tab pane architecture reveals how the application maintains stable identities, handles dynamic window splits, and keeps layout calculations pure and unit-testable without touching PTY code.

The Three-Layer Stack

Herdr’s terminal UI follows a strict delegation model across three architectural boundaries:

Layer Responsibility Primary File
Workspace Logical collection of tabs, git metadata caching, and public pane numbering src/workspace.rs
Tab Single pane tree ownership, runtime spawning, and pane state tracking src/workspace/tab.rs
TileLayout Pure BSP geometry calculations, split logic, and focus management src/layout.rs

Each layer communicates through explicit data structures—Workspace holds Vec<Tab>, each Tab owns one TileLayout, and TileLayout computes coordinates without side effects.

Workspace Layer: The Container

The Workspace struct in src/workspace.rs serves as the top-level entity that survives terminal session restarts. It tracks identity through a stable id string and derives display labels from identity_cwd.

Key responsibilities include:

  • Tab management – Stores tabs in pub tabs: Vec<Tab> and tracks the visible surface via pub active_tab: usize
  • Git integration – Caches repository state in cached_git_branch, cached_git_ahead_behind, and cached_git_space to avoid filesystem thrashing during UI renders
  • Public pane numbering – Maintains public_pane_numbers: HashMap<PaneId, usize> to provide compact, user-facing indices that are recycled when panes close
  • Delegation – All mutating operations (create_tab, close_tab, move_tab, split_focused, close_focused) forward heavy lifting to the active Tab instance, then update workspace bookkeeping
pub struct Workspace {
    pub id: String,
    pub custom_name: Option<String>,
    pub identity_cwd: PathBuf,
    pub tabs: Vec<Tab>,
    pub active_tab: usize,
    // Git caches and pane number maps omitted for brevity
}

Tab Layer: The Pane Tree

Each Tab in src/workspace/tab.rs represents a distinct layout surface within a workspace. It bridges the immutable BSP geometry with the mutable terminal runtimes attached to each pane.

Critical structures include:

  • root_pane: PaneId – The entry point into the binary space partition tree
  • layout: TileLayout – The pure BSP engine that determines where panes appear on screen
  • panes: HashMap<PaneId, PaneState> – Maps geometric pane IDs to runtime state (terminal ID, scrollback, visibility)
  • Runtime isolation – Production code accesses the global TerminalRuntimeRegistry, while test code may use an internal runtimes: HashMap<PaneId, TerminalRuntime>

The Tab handles concrete user actions like split_focused(), close_focused(), and split_focused_command(), translating them into TileLayout mutations and spawning new TerminalRuntime instances via TerminalRuntime::spawn*.

pub struct Tab {
    pub custom_name: Option<String>,
    pub number: usize,
    pub root_pane: PaneId,
    pub layout: TileLayout,
    pub panes: HashMap<PaneId, PaneState>,
    pub zoomed: bool,
    // Event channels and render notifications omitted
}

Layout Layer: Pure BSP Geometry

TileLayout in src/layout.rs embodies Herdr’s state-is-separated-from-runtime principle. It is a pure data structure containing a Node tree (either Pane or Split) and a focus: PaneId pointer.

All geometry calculations—panes(area) and splits(area)—are pure functions returning PaneInfo and SplitBorder objects without IO. Mutations like split_focused(), close_focused(), and resize_focused() only adjust the internal tree structure and focus pointer, making the layout engine fully unit-testable without mocking PTY devices.

pub struct TileLayout {
    root: Node,
    focus: PaneId,
}

Interaction Flows

Creating a Workspace

When the CLI initializes a session in src/app/creation.rs, it calls Workspace::new(), which constructs the first tab via Tab::new() and returns a tuple of workspace state, terminal state, and the async runtime handle.

let (ws, term_state, runtime) = Workspace::new(
    std::env::current_dir()?,
    rows,
    cols,
    scrollback_limit_bytes,
    theme,
    shell_cfg,
    app_events,
    render_notify,
    render_dirty,
)?;

Splitting a Pane

Horizontal or vertical splits flow through the workspace into the tab’s layout engine:

let new_pane = workspace.split_focused(
    Direction::Horizontal,
    rows,
    cols,
    None, // Use current cwd
    scrollback_limit_bytes,
    theme,
    shell_cfg,
)?;
workspace.register_new_pane(new_pane.pane_id);

Workspace::split_focused delegates to Tab::split_focused, which calls layout.split_focused() to mutate the BSP tree and inserts a new PaneState into the panes map. The workspace then registers the compact public number.

Closing and Cleanup

When closing the focused pane, Workspace::close_focused() determines whether to destroy the entire tab or just prune one pane:

if workspace.close_focused() {
    // Workspace became empty; UI can exit or delete the workspace
}

If panes remain, Tab::close_focused() removes the entry from both layout and the panes map, returning a DetachedPane containing the terminal ID for runtime cleanup.

Switching Tabs

Workspace::switch_tab(idx) updates active_tab and iterates through the newly visible tab’s panes, marking each seen = true so the UI can highlight new content.

Key Files and Responsibilities

  • src/workspace.rs – Workspace orchestration, tab lifecycle management, public pane numbering compaction, and git status caching
  • src/workspace/tab.rs – Pane tree manipulation, runtime spawning, split/close operations, and pane-to-terminal mapping
  • src/layout.rs – Immutable BSP engine (TileLayout), geometry calculation, and split/resizing logic without side effects
  • src/pane.rs – PaneState definition, storing terminal IDs and visibility flags
  • src/terminal/runtime.rs – Async process management and PTY spawning used indirectly by Tab
  • src/app/creation.rs – High-level CLI entry point for workspace instantiation

Summary

  • Herdr uses three strict layers—Workspace (container), Tab (tree), and TileLayout (geometry)—to separate concerns.
  • Workspaces own identity and metadata, including stable IDs, git caches, and compact public pane numbers.
  • Tabs manage runtime state, mapping geometric PaneIds to TerminalIds while delegating layout to pure BSP logic.
  • Layout is pure and testable; TileLayout in src/layout.rs calculates geometry without PTY dependencies.
  • All mutations delegate downward; workspaces update bookkeeping only after tabs confirm successful layout changes.

Frequently Asked Questions

How does Herdr keep its layout calculations testable?

TileLayout in src/layout.rs is a pure data structure with no async code or OS dependencies. All geometry functions like panes(area) and splits(area) return immutable structs (PaneInfo, SplitBorder), allowing unit tests to verify BSP behavior without spawning real terminals.

What is the purpose of public pane numbers?

The public_pane_numbers map in Workspace provides stable, user-facing indices (1, 2, 3) that remain compact even when internal PaneIds are UUIDs or large integers. When close_focused() removes a pane, the workspace renumbers remaining public indices to avoid gaps.

What happens when the last pane in a tab closes?

Workspace::close_focused() checks the pane count before delegating. If the active tab would become empty, it triggers close_active_tab_and_report(), which may destroy the entire workspace if no tabs remain, signaling the UI to exit or switch contexts.

How does Herdr separate state from terminal runtime?

The architecture isolates PTY code in TerminalRuntime (spawned in src/terminal/runtime.rs) while keeping Workspace, Tab, and TileLayout as plain data structures. A Tab tracks pane state in a HashMap<PaneId, PaneState> but only holds TerminalRuntime references in test configurations; production code uses the global registry, ensuring layout logic never blocks on IO.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →