Files
paperclip-harness/docs/UI-LAYOUT.md

440 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The interface: what we need, how it is grouped, and how it is drawn
Written 2026-11-10. This replaces the earlier layout note: it is now the one document that
says what the interface contains. Layout comes after this, and is chosen from it.
Plain English throughout. No invented shorthand. If a word here is not obvious, it is
defined in "Words we use".
---
## Words we use
- **Workspace** — one page in TUIOS. The person switches between workspaces.
- **Panel** — one pane inside a workspace. Each panel is a program, or one area of one
program, that holds one kind of content.
- **View** — a way of looking at a panel, for example "the task tree" or "the diff". A
panel can hold several views and show one at a time.
- **Focus** — which panel receives the keys. Exactly one panel has focus at a time, and
the person can always see which one.
- **Fact** — a piece of truth with exactly one owner. Tasks, blockers, comments, runs,
costs and worktrees belong to Paperclip.
A workspace holds as many panels as the work in it needs. **Four side by side is a good
default**, because more than four at once stops being readable — but it is not a rule.
When a workspace needs a fifth thing, there are three honest choices, and all of them are
allowed:
- **a tab** inside an existing panel, for another view of the same kind of content;
- **a tool panel** that opens beside the others and closes again when it is done;
- **zoom**, which is TUIOS's own key, for the moment a panel needs the whole screen.
The only rule is that everything in a workspace belongs to the same question. If a panel
does not help with that question, it belongs in another workspace.
---
## How things are grouped
The grouping is by what the person is doing, not by which system the data came from. There
are four things a person does with this factory:
1. **Decide and direct.** What is the state of the work, what is blocked, what is waiting
on me, what do I do next.
2. **Watch.** What is an agent doing right now, what has it changed, what has it cost.
3. **Talk.** Answer an agent, correct it mid-task, unblock a task with words.
4. **Author and govern.** Write a new task or plan, set the verify command, watch the
budget, repair the layout when TUIOS loses it.
That gives six workspaces. They are deliberately fewer than the number of things we could
show, because a person who cannot find the screen they need has gained nothing from more
screens.
| Workspace | The question it answers | Panels (a good default set, not a limit) |
|---|---|---|
| **Board** | what is the state of the work, and what needs me | Task tree · Task summary · Needs you |
| **Work** | what is happening now, and what changed | Run log · Diff · Worktree facts |
| **Conversation** | what has been said, and what do I say back | Thread · Composer · Task header |
| **Author** | what am I asking for, and how will it be judged | Task form · Guidance · Preview |
| **Project** | which folder, which machine, what limits, what cost | Project facts · Costs · Settings |
| **Repair** | is the layout sound, and what went wrong | Layout map · Health · Log |
The **shell** workspace stays as it is: one pane with a prompt in the project folder, for
the moments when the person needs a shell and should not have to leave.
Inside a workspace, panels are either **side by side** or **stacked as tabs**, and the
person switches between tabs without leaving the workspace. A tab is not a second-class
panel: it has the same frame, the same footer and the same focus marker as any other.
## Tools that open as panels
Several things the person needs already exist as good terminal programs, and wrapping them
would be worse than opening them. Each opens in the current workspace, next to what is
already there, and closes when it is done. This is the "many panels can simply be terminal
tools" idea from the product statement, done deliberately rather than by accident.
| Tool | What it is for | Opens with |
|---|---|---|
| **Lazygit** | branches, commits, staging, history, in the project folder | `t` then choosing it |
| **Hunk** | reading a pull request or a branch change as a review | `t` then choosing it |
| **shell** | any command the panel does not cover | `t` then choosing it |
| **git log and diff** | the task's branch without leaving the interface | `t` then choosing it |
| **ocr** | the review of the current change, as it runs | `t` then choosing it |
Both Lazygit and Hunk are already installed on this machine, and Hunk is a flake input of
the system configuration. Neither is ours to rebuild.
---
## The user interface items
These are the pieces the panels are made from. Each one says what it is for, which
Charmbracelet component implements it, and what it must show in its empty, loading and
failed states. The list is deliberately finite: if an item is not here, a panel does not
invent it.
### Navigation and framing
**1. Workspace strip.** A one-line strip at the top of every panel showing which
workspace this is and how many there are, with the current one marked. Implemented in
Lipgloss; TUIOS switches workspaces, so this is a position indicator, not a control.
Empty state: never empty. Failed state: never applicable.
**2. Panel frame.** A border with a title and a focus marker. Rounded border when the
panel has focus, plain border when it does not, plus the word `focus` in the title so the
difference does not depend on colour. Implemented with Lipgloss borders. Every panel has
one, and the frame is what the "too small" message replaces.
**3. Footer key hints.** The keys that work right now, in this panel, in this state. Uses
the `help` component with `key.Binding`, so a key that does not apply is not shown.
Never shows a key that would do nothing.
**4. Notice line.** One line for the result of an action, such as "sent", "created
AUD-166", or the exact error text. Disappears when the next action starts. Uses Lipgloss
only; the text is always the server's own words, never a paraphrase.
**5. Stale marker.** The time of the last successful read, shown in the panel title. The
purpose is honesty: a panel that has not read recently says so instead of looking current.
### Lists and rows
**6. Task row.** One line per task: selection cursor, identifier, status, assignee, title,
and a blocker badge when something blocks it. The `list` component with a custom
delegate, so filtering and scrolling come for free.
**7. Group header.** A heading with a count, such as "Blocked (2)". Not selectable.
Implemented in the list delegate.
**8. Status pill.** The task's state as a short word with a distinct style per state, and
a different shape as well as colour so it survives a monochrome terminal and colour
blindness.
**9. Assignee chip.** The agent's name, or a dash. Uses the agent name from Paperclip,
never an identifier fragment.
**10. Blocker badge.** `waits on AUD-163` on the row, read per task from the blocker
diagnosis. When the blocker is finished the badge says `ready`, because a task that is
ready but still marked blocked is the most confusing state we have.
**11. Needs-you card.** One card per decision waiting on a person: a refused merge, a
task waiting for an answer, a task whose blocker finished while it was still marked
blocked. Each card says what happened, what is expected, and the one key that acts on it.
Implemented as `list` items with a different delegate.
**12. Table.** For figures that must be compared: per-task cost, per-run duration,
per-file change counts. Uses the `table` component with fixed column widths taken from the
panel width, never from a constant.
### Reading long content
**13. Thread message.** Author, time, and the body with paragraph breaks and code blocks
preserved. Bodies are wrapped to the panel, never truncated with an ellipsis in the middle
of a sentence. Scrolls inside a `viewport`.
**14. Composer.** A multi-line text area for the reply, growing with the text up to a
third of the panel, then scrolling. Uses `textarea`. Enter sends and needs to be stated in
the footer, because a text area normally treats Enter as a new line.
**15. Diff view.** File headers, hunks, and line numbers, with additions and deletions
distinguished by a leading character as well as colour. Scrolls inside a `viewport`.
Very large diffs are capped with an explicit line saying how many lines were left out, and
a key that shows everything.
**16. Run timeline.** One row per run: state, what woke it, start and end, duration, cost,
and a link to the agent's final message. Uses `table` or a list delegate.
**17. Worktree facts.** The folder, the branch, the base it was cut from, and the
delivery state. Small, monospaced, never wrapped.
### Input and forms
**18. Form field.** A label, an optional "(required)", a hint line under it, and the value.
Single-line fields use `textinput`; the description uses `textarea`. Tab moves forward,
Shift-Tab moves back, Enter on the last field submits.
**19. Validation message.** Directly under the field that is wrong, in the field's own
words, and the submit key stays available so the person can correct and try again.
**20. Result receipt.** After a write: the identifier that was created or the state that
was set, with the server's response behind it. A failure shows the server's message
verbatim, because paraphrasing an error we do not understand is how an afternoon is lost.
**21. Confirmation prompt.** For anything destructive: the exact thing being removed, and
the word that must be typed to continue. Uses `textinput` with no suggestions.
### Status and feedback
**22. Spinner with a verb.** While a read or a write is in flight: what is happening, not
just that something is. Uses `spinner`.
**23. Progress bar.** Only for things with a known count, such as "3 of 7 tasks read".
Uses `progress`. Never used as decoration, and never used to fake a duration.
**24. Health row.** One line per check in the Repair workspace: name, pass, fail or not
run, and the reason. Three states, never two, because "not run" is not "pass".
**25. Empty state.** Every panel says why it is empty: "no tasks in this project", "this
task has never run", "no worktree, so nothing to compare". An empty panel with no words is
a bug.
**26. Error state.** The failed thing, the exact message, and the one action that is still
possible, such as retry or open the Repair workspace.
### Working with several panels
**28. Tab strip.** For a panel holding more than one view: a single line of named tabs
with the current one marked, switched with `[` and `]` while the panel has focus.
Implemented in Lipgloss and styled like the panel frame. A tab name is a word, never a
number, so it survives being read aloud.
**29. Tool panel.** A panel that runs an existing terminal program — Lazygit, Hunk, a
shell, a git command, or the running review. It opens on demand in the current workspace,
takes focus when it opens, and gives focus back when it closes. It runs the real program,
so it is not styled by us and does not pretend to be.
**30. Panel closer.** Every tool panel says how to close it in its own footer (`q` or
`esc`), and closing it returns the workspace to exactly the arrangement it had before,
with focus where it was.
### Icons
**27. Glyphs.** A small set used consistently: running, waiting, blocked, done, failed,
unread. Never the only carrier of meaning.
Measured, then fixed: no Nerd Font was installed and Starship was not either. JetBrains
Mono Nerd Font is now declared in `/etc/nixos/configuration.nix` (so `.13`, `.27` and
`.51` all get it on rebuild) and Ghostty's `font-family` points at it. Until that rebuild,
TUIOS's `default` glyph set — ordinary Unicode such as `╭ ╮ ✕ ▎ ● › …` — is what renders,
with `ascii` as the fallback. The program therefore uses plain Unicode symbols today and
may adopt richer glyphs once the font is present, but never depends on them.
---
## What each panel shows, and where the facts come from
| Panel | Shows | Reads from |
|---|---|---|
| Task tree | groups, rows, badges | company issue list, then the blocker diagnosis per task |
| Task summary | status, assignee, priority, blockers, verify command, cost, last run | single task read, blocker diagnosis, cost summary |
| Needs you | decisions and waits | task states, merge refusals, pending answers already on the task |
| Run log | run timeline and activity | runs, activity |
| Diff | changed files and the change | `git` in the task's own worktree |
| Worktree facts | folder, branch, base, delivery state | the task's current execution workspace |
| Thread | messages | comments |
| Composer | the text being written, the status to send | the person, then a comment or a status change |
| Task header | identifier, title, status, assignee | single task read |
| Task form | the fields being written | the person, then a created task |
| Guidance | what a good task needs, and the project's verify command | the project's `.harness` file |
| Preview | the description as the agent will receive it | the form, not the server |
| Project facts | folder, machine, session, project identifier | the project's `.harness` file |
| Costs | spend by task and by agent | cost summary per task, agent budget |
| Settings | verify command, budget, watched task | the `.harness` file, then Paperclip for what it owns |
| Layout map | workspaces and panels as they actually exist | TUIOS |
| Health | checks and their outcome | the checks themselves |
| Log | what the repair did | the repair's own output |
Two rules follow and are not negotiable:
- **A panel never invents a fact.** If Paperclip does not have it, the panel says it does
not have it.
- **A panel shows when it last read.** So a wrong panel is visibly stale rather than
quietly wrong.
---
## How it is drawn
**One theme, semantic roles.** Colours are named by role (text, muted, accent, success,
warning, error, and a surface colour), and panels ask for the role, never for a hex value.
A dark and a light variant exist from the start.
**Focus is unmistakable.** The focused panel gets a rounded border, a bright border colour,
and the word `focus` in its title. Two of those three do not depend on colour.
**Density.** One cell of horizontal padding inside a frame, one blank line between
sections, no blank line between rows. Rows are single lines except thread messages, which
are the only content that gets paragraph space.
**Alignment.** Identifiers, statuses, assignee names and numbers are aligned into columns
whose widths are computed from the content in view, capped at a share of the panel width.
Never a hardcoded column width.
**Truncation.** A truncated title ends in an ellipsis and the full text is available with a
key that opens a detail view. Nothing important is truncated silently.
**No decoration that carries no meaning.** No gradients, no box-drawing around every row,
no colour that does not correspond to a state.
**Motion.** Only two things animate: the spinner while something is in flight, and the
selection cursor as it moves. Nothing else moves on its own.
**The Charmbracelet pieces we use:** `list` for rows and decisions, `table` for figures,
`viewport` for anything scrollable, `textarea` for the composer, `textinput` for fields
with one line, `help` and `key` for the footer, `spinner` and `progress` for feedback.
Tabs are not a component in this collection, so a view switcher inside a panel is built
from Lipgloss and the `help` component, and is styled like the rest of the interface.
---
## Keys
TUIOS keeps `h`, `j`, `k`, `l`, `z` and `n` in window management mode, so no panel may use
them. A bare Esc must stay free, because it closes a box.
| Where | Key | Does |
|---|---|---|
| Any panel | Tab, Shift-Tab | move focus to the next or previous panel |
| Any panel | [ and ] | previous or next tab, in a panel that has tabs |
| Any panel | t | open a tool panel here: shell, Lazygit, Hunk, git, review |
| Any panel | ? | show the keys for this panel |
| Any panel | r | read again from Paperclip |
| Any panel | q | leave this panel's program, returning the pane to its shell |
| Any panel | Esc | close the open overlay or leave the composer, never quit |
| Task tree | Up, Down | move the selection |
| Task tree | Enter | open this task in the Conversation and Work workspaces |
| Task tree | a | assign to the configured agent |
| Task tree | Space | expand or collapse a group |
| Thread | Up, Down, Page Up, Page Down | scroll |
| Composer | Enter | send |
| Composer | Shift-Enter | new line |
| Composer | s | choose the status to send with the reply |
| Diff | Up, Down, Page Up, Page Down | scroll |
| Diff | f | show the whole file, not the capped view |
| Forms | Tab, Shift-Tab | next or previous field |
| Forms | Enter | next field, and submit on the last one |
| Destructive | typed word | confirm |
No key means two things in two panels. Where a panel needs a key that is taken, it uses a
different one rather than overloading. **Zoom is not ours**: `z` belongs to TUIOS, and the
interface relies on it rather than reimplementing it. The same is true of switching
workspaces: TUIOS does that, and the interface only shows where you are.
---
## Size, and what happens when there is not enough
Every panel declares a smallest useful size. When a panel is smaller than that, it draws
its own short message with the size it has and the size it needs, and nothing else. The
program never draws a broken layout and never invents a scrollbar to hide the problem.
| Panel | Smallest useful size |
|---|---|
| Task tree | 30 columns by 10 rows |
| Task summary | 30 by 8 |
| Needs you | 30 by 6 |
| Run log | 40 by 10 |
| Diff | 40 by 12 |
| Worktree facts | 30 by 6 |
| Thread | 40 by 12 |
| Composer | 40 by 4 |
| Task form | 50 by 14 |
| Guidance | 30 by 6 |
| Preview | 40 by 10 |
| Project facts, Costs, Settings | 40 by 8 |
| Layout map, Health, Log | 40 by 8 |
The workspace itself needs 80 columns by 24 rows. Below that, the whole workspace shows one
message naming the size it has and the size it needs.
Tabs change the arithmetic in a useful way: two panels tabbed into one cost the space of
one panel, so a narrow workspace keeps its content by tabbing rather than by shrinking.
That is why tabs are part of the design and not a workaround.
---
## How the panels sit together
Layout follows the width of the terminal, so the person gets as much as their screen can
carry and never a layout pretending to have room it does not.
- **150 columns and wider:** Board shows the task tree and task summary side by side, with
Needs you tabbed under the tree. Work shows the run log beside the diff. Conversation
shows the thread above the composer, with the task header above both.
- **110 to 149 columns:** at most two panels side by side; the third becomes a tab in the
panel that holds the same kind of content.
- **80 to 109 columns:** one panel at full width, with the others tabbed beside it, in the
order the work happens: Board, Work, Conversation, Author.
- **Below 80 by 24:** one message, nothing else.
Two arrangements we already know we want, because they are the moments the person is
actually in:
- **Reading a claim and its code.** Task tree on the left, thread on the upper right, diff
on the lower right. The only arrangement where a claim and the change that backs it are
on screen together.
- **Deciding.** Needs you on the left with the task it concerns beside it, so the decision
card and the evidence are never on different workspaces.
## What happens when you pick a task
Plainly: **choosing a task changes what the other panels show.** Pick a task in the task
tree, and the summary, the thread, the run log, the diff and the worktree facts all switch
to that task. Nothing else has to be typed, and no screen has to be told twice.
The rest is implementation, and it is written down only so the behaviour can be trusted:
- the chosen task is remembered in the project's own `.harness/harness.toml`, as one line
naming its identifier;
- the other panels re-read when that line changes, within about a second;
- the choice survives a restart or a lost pane, because it is on disk and not in memory;
- if the chosen task is ever deleted, the panels fall back to the most active task in the
project rather than showing an error.
That is the whole mechanism. It is one line in one file, it is not a second copy of
anything Paperclip owns, and it can be inspected with a text editor when something looks
wrong.
## What is not in this version, deliberately
- No chat with an agent as a separate thing: the Conversation workspace is the chat.
- No plan editor: a plan is a task's description, and the Author workspace writes it.
- No second task board: TUIOS has one, Paperclip has one, and this interface is the one
that knows about blockers and worktrees.
- No settings for colours: one theme, two variants, no per-panel colour choices.
- No dashboards of figures with nothing to act on: every number sits next to the thing it
describes.
- No replacement for Lazygit or Hunk: they open as tool panels.
## Decided, and what is still open
Decided by the person, 2026-11-10:
1. **Switching inside a workspace is wanted.** Tabs and zoom are part of the design, and a
workspace may hold more than four panels when the work in it needs them.
2. **Grouping matters more than panel count.** A workspace earns its place by answering one
question well.
3. **Picking a task updates the other panels.** That is the behaviour described above.
4. **Lazygit and Hunk belong inside the interface**, as tool panels, not as things the
person leaves for a shell.
Still open:
1. **Glyphs.** Measured: TUIOS has `ascii`, `default`, `heavy` and `unicode`; `default` is
active. Starship is not installed here and no Nerd Font is present. So the interface
uses the `default` set with an `ascii` fallback unless a Nerd Font is installed on
purpose. If Starship's look is wanted, that is a system change first, then a glyph set.
2. **Which panel wins when the terminal is narrow.** Proposal: the one that answers the
workspace's question, with the rest tabbed rather than hidden.
3. **The order of tabs** in each workspace. Proposal: the order in which the work happens —
what it is, what it says, what it changed.