Skip to content

feat(ui): move between regions, name rows by identity, sort from the column - #51

Open
Brad (bmethod) wants to merge 5 commits into
issue-24-phase-7from
issue-24-design
Open

feat(ui): move between regions, name rows by identity, sort from the column#51
Brad (bmethod) wants to merge 5 commits into
issue-24-phase-7from
issue-24-design

Conversation

@bmethod

Copy link
Copy Markdown
Collaborator

All of #38: the second design pass, and the last of the #24 work. Stacked on #50.

Replaces #45, which was opened against the phase-7 branch and never drew a review: this repository's automatic reviewer fires on a pull request's original open, and reopening one it has never reviewed does not re-arm it. The branch is unchanged; only the pull request is new.

The owner ran the app after the first design pass and asked for a second. Every item below was re-checked at 180x50 against a tree holding all seven phases, after a 200-second run so the charts held real history — anything already fixed was struck rather than filed.

Move between regions, not just within one

Home holds four regions and the keyboard reached them as one flat list. Arrows move within the focused region along its own axis; Tab and Shift+Tab move between regions. Left and right run along the horizontal tile row, up and down through a list, and Timeline keeps left and right for its time cursor.

The first draft had left and right leaving the region, which reads as one rule and is not: it makes moving along a horizontal row of tiles a matter of pressing down, and Timeline could not follow it at all because those keys already mean the cursor there. The agent building it flagged that rather than picking the nicer-feeling option, and the rule it exposed is the one shipped.

Tab no longer changes screens anywhere. The alternative was Tab moving regions on the two screens that have them and changing screens on the other five, which is the second rule this change exists to remove. The number keys 1-7 remain and are printed across the header at all times; the help panel now states them, since that route had been implied by Tab. A test pins that Tab leaves a single-region screen alone.

Regions exist on Home and Storage, the only screens with more than one selectable list.

A child line looks like a child

A vertical rule at the left of every continuation line, keeping the indent — drawn as a box's own left border, so it runs the full height of its content without anyone counting lines. Four sites through one helper: the concern detail, the capability diagnostic, the Storage drill-down, the agent detail sections.

One pattern for one idea

collapsed, open, the count beside the name. Searching the UI sources for those glyphs finds them in widgets.tsx alone; five sites draw through one component, and a row draws its marker whether or not anything is open — so a reader can see what is worth pressing Enter on without pressing it.

Hold the order while reading

Busiest agents re-sorted every second and the reader's target moved under the cursor. o holds the row order while the values keep updating: a dashboard that freezes its data lies while you read it. A lane that ended drops out; a lane that climbed does not push in. The hold releases on leaving the screen. o rather than p, because pinning a sample and holding an order are different things.

An id is a column, not part of a name

The same list read .hclaude claude ken-1295 beside method PID 3405 beside .2claude claude dotfiles PID 2557288 — correct behaviour, since the id appeared only where names collided, and unreadable presentation, because to a reader it looks arbitrary.

The name column holds the name; a dim id column holds the process id for every row. distinguish is gone from the lane model, so no name is appended to.

Every list that draws lane names carries the column — Home, Agents and Builds. The builds lane model had no process id at all and carries one now. The agent detail already names its process in its identity line, which is where that belongs for a screen showing one lane, and a test holds it there.

The guard pins the property rather than the column: six lanes named method with every reading identical, rendered at two widths, and no two drawn rows may read the same. A future narrowing that drops the column fails it whatever the column is called.

Where Home's width cannot keep the name readable, the state column goes instead. A blocked or running lane already shows in its numbers; a name that identifies nothing shows in nothing.

Sort from the column

The sorted heading carries or , chosen in one place. On a numeric column the arrow leads, so the heading still ends where its digits end — putting it after the label broke the alignment an earlier phase established.

Builds and Resources do not sort, each for a reason rather than by omission: Builds' rows are not lanes and have no comparator, and Resources is a tree that sorting would flatten.

Say what unread means

57 sources vsys cannot read named a mechanism and no consequence. It now names what a reader loses — every wait reading is blank rather than zero, and which screens those are. The interface and the system's own words stay in the drill-down, which is where someone goes to fix it rather than to understand what they are missing.

Separate what is separate

Recent changes truncated mid-sentence with no mark. It has three columns now, and its subject cuts through the same helper every other cell uses.

Proof

399 tests, 0 fail.

This line read 385 while the branch sat under #43 and #44. The stack was replayed onto the merged phase-5, which carries four tests the review rounds added there, and 385 was itself one too high — a duplicate entered during the test-file split and overstated every figure from that commit until it was found. #43 records why only the count could see it.

Plants throughout, and three of them caught a test rather than the code:

  • a cut-mark assertion that passed when a row ended in whitespace — and the renderer pads every line to the terminal width, so it passed whatever the code did;
  • a tile test that only ever pressed right, so a plant on left changed nothing observable;
  • an identity fixture whose six rows differed by their CPU numbers, so the rows looked distinct while the names still named nothing.

One decision came from a capture rather than a test. The Agents list sheds optional columns as it narrows, ordered trend, id, pane — identity last, which sounded right. The 180-column frame showed an empty pane column surviving while every process id was dropped. The order is now trend, pane, id: identity that always exists outranks identity that usually does not. Four widths are pinned.

Closes #38. Part of #24.

https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@bmethod
Brad (bmethod) force-pushed the issue-24-design branch 6 times, most recently from bce9925 to 7cb69c0 Compare September 10, 2026 19:25
A screen's lists are regions of one flat selection. Left and right move between
them, up and down move inside the one in focus, and a region with no rows is
skipped because a reader cannot stand on a row that is not there. The focused
region's title takes the accent colour and its rule stops being dim, so nobody
has to press a key to find out where they are. Home has four regions and its
tile row is one of them, which makes the tiles the same rule as everything else
rather than a second one. Storage has three.

A line that expands under another now carries a rule down its left edge as well
as its indent. The indent alone reads as a new top-level line at a glance, and
it is the rule that says whose line this is. One helper draws the concern
detail, the capability diagnostic, the Storage drill-down and the agent
detail's open sections.

One marker says a row has more inside it: `▸` closed, `▾` open, the count
beside the name. It is drawn whether or not anything is open, so a reader can
see what is worth opening without opening it. The agent detail's sections, the
Settings capability rows and unreadable sources, the Home concerns and the
Storage filesystems all draw through it.

`Busiest agents` re-sorts every second and a reader's target moves under the
cursor. A key now holds the row order while every reading in those rows keeps
updating. Holding the order is not freezing the data: a dashboard that lies
while it is read would be worse than one that moves. A lane that ended drops
out, a lane that climbed does not push in, the section says it is holding where
its count sits, and leaving the screen releases it.

Claude-Session: https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB
A name holds the name. Nothing is appended to make one unique any more: an id
that appears on some rows and not others reads as arbitrary, so the process id
is a column of its own on every row, beside the pane address phase 7 added.
Two lanes with one name are told apart by scanning a column. A narrowing list
sheds its optional columns in one order until the name has its floor back: the
trend first, then the pane address, and the id last, because the id is the one
that is on every row.

A sorted column says so in its own heading. The arrow leads on a numeric
column, so the heading still ends where the digits under it end, and follows
the word on a text column. Each screen sorts the headings it draws, which is
why Home's `Busiest agents` cycles its own four rather than the Agents column
set: a marker has to sit on a heading the reader can see.

An unread source says what it costs instead of which interface was missing.
The readings it feeds are blank rather than zero, and the row names where those
readings are. Blank and zero are different answers, and a dashboard that shows
zero for a number it could not read is lying. The interface and the system's
own words stay in the drill-down, which is where a reader goes to fix it. The
collapsed group of unreadable sources now sits at the depth of the rows it
summarises rather than two columns to their left.

`Recent changes` gives its time, kind and subject their own columns, so three
rows scan as three rows and a cut subject ends in its mark instead of stopping
mid-word.

Claude-Session: https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB
An arrow moves within the focused region, along that region's own axis: left
and right along the horizontal tile row, up and down down a vertical list, and
left and right along Timeline's time cursor, which is what that region
contains. Tab and Shift+Tab move between regions.

That is one rule rather than two. The earlier draft gave left and right the job
of leaving a region, which could not be universal: those arrows already move
the time cursor inside Timeline's own region, and a horizontal row of tiles was
left navigating vertically.

Tab no longer changes screens anywhere. Every screen has a number key and those
are printed across the header at all times, so the discoverable route to a
screen is not the one being repurposed, and a key that moved regions on two
screens and changed screens on the rest would be the second rule this removes.
The footer and the help panel say so, and the help panel now names the number
keys as the way to a screen.

Claude-Session: https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB
Taking the disambiguator out of lane names moved the problem rather than
removing it: Home's busiest agents and the Builds lanes had no id column to
put it in, so six rows read `method` with nothing beside them. Ambiguous but
labelled became ambiguous and silent.

Both lists now carry the same dim id column the Agents list has, and
`LaneBuilds` carries the process leading its lane so the Builds row has one to
draw. The agent detail already named its process in the identity block, which
is where one lane at a time belongs; a test now holds it there.

Home shares its width with the column beside it, so when the name cannot keep
its floor the state column goes instead. A blocked or running lane already
shows in its numbers; a name that identifies nothing shows in nothing.

The guard is the property that failed rather than the column that fixes it: six
lanes named `method`, every reading identical, rendered on Home, Agents and
Builds at two widths, and no two drawn rows may read the same. A future
narrowing that drops the column fails it whatever the column is called.

Claude-Session: https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB
Six lanes named `method` drew as six rows a reader could not tell apart at
37 of the 127 terminal widths between 84 and 210. Reproduced before
anything changed, by arithmetic and again by rendering every width: the
runs are 84 to 106 and 150 to 163, and both agree exactly.

The second run is the one worth knowing. The side pane opens at 150 and
takes a third of the terminal, so a wider terminal makes the list
narrower: at 150 the list is 97 columns against 120 at a terminal width
of 120.

The principle this branch already states is that identity that always
exists outranks identity that usually does not. One step further:
identity outranks readings. So the process id leaves the optional set
altogether and `Program`, which reads `claude` on every row, and `Wait`,
which reads `0.0%` on every row, take its place in it. The order is now
trend, program, wait, pane address, and the id is never given up.

Measured against the two alternatives rather than argued. Making the id
fixed alone closes both windows but leaves the name column at 17 columns
at a terminal width of 100, under its floor of 24. Moving only the
readings below the id closes one window and leaves 20 widths collapsed,
because the id is still not asked for at all below a list width of 100.
Doing both closes all 37 and holds the name floor everywhere. And with
the pane column wanted, ranking it above the readings keeps it at every
width, where ranking it below loses it at 52 widths and keeps `Wait`
instead. A name that falls back to carrying its id was not taken: it is
the appended id this branch exists to remove.

The guard for that property could not fail for it. It rendered 120 and
180, one width from each safe band, so the narrowing it was named for
already existed and it passed. It now renders 99, 106, 120, 163 and 180
and reads the ids out of the column by its heading offset, because
comparing whole rendered lines is unsound above 150: at 163 the side pane
text beside four identical rows made them look distinct, and whole lines
gave 6 of 7 where the column held six rows carrying no id at all.

The two byte-identical tests of the shedding order are now one, named for
what it checks. The one that survived states the new order; the other
stated the rule the diff had already reversed.

Moving back to the tiles left them off screen: the scroll effect named
`selected` and setting `tile` re-ran nothing, so the frame did not move
and the only visible change was the marker disappearing. Enter then
opened another screen with nothing on this one saying so.

That is the third screen with this cause, so the shape is now one hook
rather than a third copy. It asks the drawing two questions instead of
enumerating state. The reader moved: the target's id is not the one it
was, so go to it. The drawing moved under a reader who did not: the id is
the same and the target sits elsewhere in the content, so chase it only
if it has left the screen and only once the reader has chosen something.

Three things measured rather than assumed, each having reversed a
decision. These boxes take the wheel, so scrolling on every render drags
the reader back on the next sample. A child's `y` is a screen position,
not a content one, so guarding on it fights the wheel too; the content
position is `y` plus the scroll offset. And cancelling the pending
reading on the next render starves it: a reader holding a key down
renders faster than a timeout fires, and the screen never follows them at
all.

`settings-screen.tsx` keeps its own effect. Its trigger list is the same
shape, but what it does is different arithmetic, not this scroll.

Also on Home: asking for a different order now releases the held one,
because `o` then `s` read `↓ Memory` over rows still in the held CPU
order; and a concern's marker reads the same question its detail is drawn
under, because on the tiles it drew `▾` with no rule and nothing beneath.

Eleven claims corrected to say what the code does. Three of them a user
reads: the README said Tab changes screens and listed neither the region
key nor `o`; the README and the Settings help both said vsys appends the
working directory or the process id to a name, which this branch deleted,
and a reader reaches that one by pressing 7. `ui-invariants.md` stated
the shedding order backwards, in exactly the direction this branch tried
and rejected. `ui.md` claimed the wide Agents table carries the id
column; it draws configured columns and there is no `pid` among them.

Three `Covers:` entries named files that are not there. `builds.md` and
`storage.md` now name the `-screen.tsx` files this branch changes, so the
drift check can see them; `verdict.md` named `src/ui/overview.tsx`, which
was deleted with no successor.

Controls, each reddening its own test: the branch's own column set, for
the guard and for the shed order; a Home scroll that does not know the
tiles are a place to stand; a hook that scrolls on every render; one that
reads the screen position; one with no settled reading; one that does not
follow a moving selection; one that does not chase the drawing. A clause
requiring the row to have been on screen was deleted rather than kept,
because with the reader-has-chosen bit it could not be made to fail.

415 tests, 0 fail, three runs.

Claude-Session: https://claude.ai/code/session_01B4rKWUEy6WF5MRxtBEwFdB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant