docs/specs/2026-10-01-cli-views-design.md
259 lines · 10009 bytes
CLI views
Status: accepted, 2026-10-01. Follows 2026-10-01-cli-terminal-output-design.md
(v1.41.0, #312–#315).
Problem
Terminal output after v1.41.0 is coloured and humanized but still noisy. Captured at 110 columns against gitbay.org:
- Every list and every dashboard section is the same grid under a dim
ALL-CAPS header row. There is no hierarchy; a header row costs a line
and says little (
#,!,REF,WHEN). - Cyan marks every ref, path, SHA and feed target. A
build listrow has three cyan cells, so cyan carries no information. dashboardruns to about 70 lines: 20 builds, 7 pinned repositories, 8 activity rows, and the adminServer/Queuesblocks.Queuesuses raw tabs, and its nested tables are padded inside the colour codes, so they do not align with the parent.krz/gitbayrepeats on 15 of 20 build rows; refs keep their owner prefix inside the repository they belong to.- State reads three ways:
✓ 2/2onrepo show, coloured words onbuild list, an uncolouredrunning. - Nothing says what to run next, apart from a
more:line that repeats the command with an opaque cursor.
The design bar is magit's status buffer: a header block of Label: lines,
counted section headings, short dim refs on the left, and a menu of the
actions that apply right now.
Constraints
- Piped output and
--jsonare byte-identical before and after. - Rendering stays server-side in
internal/controlbehind--term, so stocksshwith--term=<cols>,colorgets the same screens as the CLI. No formatting moves intocmd/gitbay. - Colour is never the only signal.
NO_COLOR,--no-colorandTERM=dumbdrop colour and keep glyphs and layout. - Diffs,
build logand blobs are streams, not screens. They keep the v1.41.0 rendering; only their header lines change.
Visual rules
A screen has up to four parts, in order, each separated by one blank line: a header block, a body, sections, and an action legend. Sections are separated from each other by one blank line.
Header block. Aligned Label: lines. Labels are dim and padded to the
longest label in the block; values are normal weight. The first line names
the object: Merge: !552 wire $PAGER through long views.
Body. Markup text (an issue or MR description, release notes), rendered
through termtext as view.body renders it today.
Section heading. Title (n), bold blue. n is the total, not the
number of rows shown. An empty section is omitted unless the screen marks
it as worth showing empty (Discussion (0)).
Rows. No header row, no indent. Column order: ref, glyph, text, metadata.
- Ref: the short form (
!549,#12,8f3a1c2,1779), dim, left-aligned. The owner prefix is dropped when the screen already names the repository. - Text: normal weight. The title column keeps the existing one-third-width cap for sparse columns.
- Metadata: dim, after the text, joined by
·. - A section capped by its screen ends with a dim
+12 more gitbay build list.
Colour. Follows the stylesheet's two accents.
| Role | Colour | Used for |
|---|---|---|
| What you can do | blue | section headings, legend commands |
| What wants you | yellow (--warn under truecolor) |
review requested, assigned to you, behind, needs approval |
| State | green / red / none | passed, open, signed / failed, blocked / finished, neutral |
Nothing else is coloured. Refs and paths lose their cyan.
Glyphs. First in a row or field, one meaning each, everywhere:
| Glyph | Meaning |
|---|---|
✓ |
passed, ok |
✗ |
failed, blocked |
◐ |
running, pending |
● |
waiting on you |
○ |
closed, draft |
Legend. A rule line, then up to three columns of action groups. Each
group is a bold name over its commands, blue. Below 80 columns the groups
stack in one column. Commands omit <owner/name> when it equals the repository the CLI
inferred from its clone, which the CLI sends as here=<owner/name> in
--term (an older server ignores the option). Commands may name
CLI-local commands (mr rebase, mr checkout). There is no browse:
the header's first field links to the page where the terminal shows
links, and a URL: field carries it otherwise.
Model
The existing view (internal/control/view.go) is an imperative writer
used by 18 files; it renders both piped and terminal output, branching on
Term.Cols == 0. It stays as the plain writer. The declarative model is a
new type, screen, in internal/control/screen.go:
type screen struct {
fields []field
body string // markup source
format string // body's markup format
sections []section
actions []action
}
type field struct {
label string
value []cell
}
type section struct {
title string
n int // total, shown as "(n)"
rows []row // cells, plus a markup body drawn beneath (a comment)
more []string // command for the rest, when n > len(rows)
empty bool // draw "(0)" rather than omit
}
type row struct {
cells []cell
body string
format string
}
type action struct {
group string
argv []string
}
Rows use the existing typed cells (cRef, cAge, cSize, cSwatch,
...) plus a new cGlyph(state).
A migrated command calls
c.emitView(data, plain, func() screen { ... })
Under --json it encodes data; piped, it runs plain; at a terminal it
builds the screen and renders it. emitPage gains the same form; at a
terminal its cursor becomes a final Next page action carrying the full
command, replacing the more: line.
The width logic in table.flush (fit, capSparse, dropEmpty) moves
into the section renderer. At a terminal, table renders as one untitled
section, so lists that have not migrated take the new row style in stage 1. Such a
table keeps a dim header row only when a column is a number or size
(admin runners, admin stats), which is unreadable without one.
Piped table output is unchanged. view's terminal branches are removed
once no command reaches them.
Actions are data. Each command chooses them from the state it just read:
behind the target offers mr rebase, a requested reviewer gets
mr approve, mr merge appears only when the merge gates pass, a caller
without write access gets no issue close. The renderer never fails a
command; a field without data is omitted.
Screens (stage 2)
--json for each is unchanged.
dashboard
- Header:
User,Instance. For an admin, anInstanceproblem line (✗ 1 mirror error) when one exists.ServerandQueuesleave the terminal screen;admin statscarries them. - Sections:
Review requested,Assigned issues,Your merge requests,Failed builds(last 24h, repositories the viewer can write),Recent activity(5),Pinned(refs on one line). Passing builds collapse to one dim line under activity:14 builds passed today. - Legend: the command for the first item of each non-empty section,
feed,build list, andadmin statsfor an admin.
mr show
- Header:
Merge,State,Checks,Review,Gates(eachMergeGatesgate as a glyph). - Body: description.
- Sections:
Commits(SHA, subject),Fileswith+a −d,Discussion(each comment as who · age with its body beneath; shown when empty). - Legend: Unblock (
mr rebasewhen behind), Review (mr review --approve,mr comment), Merge (when the gates pass), Read (mr diff).
issue show
- Header:
Issue,State,Labels(swatches),Assignee,Milestone,Linked(merge requests that close it). - Body: description.
- Sections:
Discussionas onmr show. - Legend: comment, assign, label, close or reopen, filtered by permission.
repo show
- Header:
Repo,Clone,Head(main ✓ 2/2),Release,Mirror(only on error). - Body: description and topics.
- Sections:
Open merge requests,Open issues,Recent commits(5). - Legend:
mr create,issue create,repo log.
build show
- Header:
Build,State,Commit,Ref,MR,Duration. - Sections:
Steps(glyph, name, duration; the failed step red). - Legend:
build log.
mr list, issue list, build list
- One section each (
Open merge requests (n), ...). - Legend:
create, the state filter not in use,Next page.
Stages
Each stage is tracked by its own issue under one tracking issue, and lands as merge requests in order.
screen.go, the renderer,emitView,cGlyph;tablerendered as an untitled section. Rewrite the Users wiki "Output rules" section.- The stage-2 screens: one MR for the show screens, one for the lists and
dashboard. - Every other
c.tableandc.viewcaller, file by file, starting withprofile,release,milestone,org,label. Removeview's terminal branches.
Out of scope: a branch-aware gitbay status (needs the CLI to send the
local branch), interactive input of any kind, and Emacs integration.
Testing
- Renderer goldens (
screen_test.go) at 80 and 120 columns, colour on and off. Every golden asserts thatstripSGRof the colour render equals the colourless render, so colour never carries structure alone. - Screens as data. Per-command tests assert on the
screenvalue, not rendered text: sections present, counts, which actions are offered for a given state and caller. - Legend commands resolve. A test passes every action argv that any
screen produces in its fixtures through the registry's
LookupandparseFlags. A renamed command or flag fails CI. - Plain output pinned. Before a command migrates, its piped output is
captured as a golden; the migration MR reproduces it byte for byte.
--jsonstays covered by the existing tests.
Compatibility
All changes are server-side behind --term. Any CLI from v1.41.0 gets the
new screens when the instance upgrades; older CLIs that send no --term
get plain output as before. Scripts see no change.