lines

The configurable rows above the input bar — a status bar you compose.

interface LinesApi {
    // Register a line. Throws if the name is taken.
    create(name: string, definition: LineDefinition): Disposer

    // Set the whole stack, top to bottom. The one place order lives.
    set(entries: readonly LineEntry[]): void

    // The single line above the conversation, or null to clear it.
    top(name: LineName, options?: { style?: LineStyle; hidden?: boolean }): void
    top(entry: LineEntry | null): void
    top(): Line | null

    // The default style for every line that does not declare one.
    style(style: LineStyle): void
    style(): LineStyle

    // Move one line. Addressed by name — an index shifts when anything above it does.
    move(name: LineName, position: "up" | "down" | number): void

    show(name: LineName): void
    hide(name: LineName): void
    toggle(name: LineName): void

    // Every registered line, in stack order.
    list(): readonly Line[]
}

type LineContent = {
    left?: LineSlot
    middle?: LineSlot
    right?: LineSlot
    style?: LineStyle     // what this line was designed for — a default, not a decree
}

type LineSlot = readonly ComponentName[] | readonly (readonly ComponentName[])[]
type LineDefinition = LineContent | readonly ComponentName[]
type LineEntry = LineName | { line: LineName; style?: LineStyle; hidden?: boolean }
type LineStyle = "plain" | "powerline" | "rounded" | "block" | "minimal"

Three layers, three owners

The split is the whole design, and it is what makes lines shareable:

LayerVerbOwnsWho writes it
Componentcomponents.createwhat the data ISan extension author
Linelines.createwhat sits whereshared or personal
Placementlines.sethow it looks herealways you

Style sits on the placement rather than the line, because a shared line would otherwise ship with its look baked in — anyone installing @cody/git-line would have to fork it to restyle. A line may still declare a default; the placement wins when it says otherwise.

A line of your own

lines.create("me:status", {
    left: ["axon:agent/name", "axon:agent/model"],
    right: ["axon:session/tokens", "axon:clock"],
})

lines.set(["me:status"])

A bare array means the LEFT slot, always — never auto-distributed. Both built-in layouts are left-anchored, so it is the common case worth a shorthand, and a rule that reads the same every time beats one that guesses.

Sections

A nested array is a section — the unit a separator divides and a background fills:

lines.create("me:full", {
    left: [
        ["axon:agent/name"],                    // outermost — the accent
        ["axon:agent/model", "axon:agent/status"],  // one step in — softer
    ],
    right: [["axon:session/tokens"], ["axon:clock"]],
})

Inside one section components share a ground, so their divider is a hairline. Between sections the ground changes, so it is a filled arrow. That distinction cannot be expressed on a flat list, which is the only reason the nesting exists.

Backgrounds step from the edge inward: the outermost section takes the accent, each step in is softer, and the innermost sits on the terminal's own ground. Position in the array is the section, so there is nothing to colour by hand — and no way for one config's bar to clash with another's.

Styles

lines.style("powerline")                            // the whole stack
lines.set([{ line: "me:full", style: "rounded" }])  // except this one
StyleLooks like
powerlinefilled arrows between sections, hairlines within
roundedthe same with round caps
blocksquare edges — the colour change is the boundary
minimala thin dot between components, no fills
plainspacing only

powerline is the default. The separator block (U+E0B0U+E0B7) predates Nerd Fonts and ships far more widely than the icon range, so a terminal that renders a git icon as a box will still draw these correctly. A terminal without even that sets lines.style("plain").

The top line

lines.top("me:nav", { style: "powerline" })
lines.top(null)     // clear it

One line, not a stack. The room above the conversation is scarce in a way the room below it is not — the bottom is already bounded by the input bar, and a second unbounded stack could push the conversation off screen.

Turning lines on and off

lines.toggle("axon:git")
lines.hide("axon:config")
lines.move("me:status", "up")

Or from the palette, without touching your config:

:lines toggle    every registered line, each row saying which way it will go
:lines on        the ones that are not showing
:lines off       the ones that are

The list stays open while you flip rows — composing a bar means changing four things and looking at the result.

show() on a line that was never placed appends it to the stack, which is what makes :lines on axon:git work on a built-in you have never mentioned.

What ships

Registered, never placed — none of these appear until you name one:

LineShows
axon:statusagent and model, its status and token use
axon:gitbranch, and what is uncommitted or unsynced
axon:contextcwd, live agent count, the clock
axon:devmost of the catalogue at once, for working on Axon
axon:spacea blank row, for deliberate spacing

Next: components — the values a line shows.