Skip to content

feat(tui): recover the #175 UI round — left rail, centred menu, framed composer - #194

Merged
sosidudku1 merged 4 commits into
mainfrom
feat/tui-ui-round
Aug 20, 2026
Merged

feat(tui): recover the #175 UI round — left rail, centred menu, framed composer#194
sosidudku1 merged 4 commits into
mainfrom
feat/tui-ui-round

Conversation

@sosidudku1

Copy link
Copy Markdown
Collaborator

Recovers the UI round from #175 onto current main, as four reviewable commits instead of a 286-file branch.

#175 was closed because it re-merged 24 already-landed PRs and would have reverted 15 files that shipped after it was cut. That reasoning still holds — but the UI round inside it was original work that exists nowhere else, and closing the PR took it with it. This is that work, rebuilt on top of everything that has landed since.

What lands

The rail moves left and becomes the app frame. It carries the brand lockup, the version and a Menu button alongside Sessions and Tasks, and it is drawn in every mode rather than chat only — switching to a panel used to take all the chrome off screen.

The menu becomes a centred overlay. position: absolute, centred on both axes, with the app behind it dimmed.

The composer becomes a framed field. A closed rounded frame whose bottom row is an action bar on the rail ground, with the model label and a clickable send →, replacing the left tail and its cap.

Every message carries [copy], and user messages carry [try again]. The clipboard layer is its own module; [try again] re-runs the text through the same submit path Enter uses.

Three places where this tree won over the branch

Deliberately excluded

The run-mode / fusion files ship in the same #175 round and are not here — that stack is still under review. The composer's formatModel keeps a local split so a two-legged label stays readable; with no fusion present it behaves exactly as before, so the fusion stack can land later without re-touching the file.

Verification

npm run lint clean. 4840 tests, 2 failures — the flaky sidecar FIFO case and the $HOME-dependent glob test, both failing on pristine main.

Every surface was checked by rendering the real Ink tree and reading the frame back, not by asserting from source: the rail on the left with its Menu button, the menu centred inside the content area, the framed composer with send →, and [copy] under a message.

Tests that encoded the old chrome are updated rather than deleted — the row-budget test still pins the 2:1 split, and the Tab-focus test still branches on whether the rail is up.

First slice of the #175 UI round, ported onto current main.

Every message carries a [copy] button; user messages also carry
[try again], which re-runs the text through the same submit path Enter
uses rather than a second code path that could drift from it.

The clipboard layer is its own module: copy-to-clipboard owns the
platform command (pbcopy / clip.exe / wl-copy / xclip), and
clipboard-context exposes it through a provider so a test can observe a
copy without touching the real clipboard. useClipboard falls back to the
shared default writer, so the buttons work whether or not a provider is
mounted.

Also lands selection-passthrough (the honest answer to mouse text
selection) and tasks-list-fit, both self-contained.

Deliberately NOT in this slice: the run-mode / fusion files that ship in
the same #175 round, since the fusion stack is still under review; and
anything touching the contested files (tui-app, menu-popup, sidebar,
theme) where main has moved since #175 was cut.

Tests: 4828 total, no new failures. The two LlmHealthPoller cases that
appear under full-suite load pass in isolation on this branch and on
pristine main.
Second slice of the #175 UI round.

The rail leads the row container, so it renders on the LEFT, and it now
carries the brand lockup, the version and a Menu button alongside
Sessions and Tasks. It is drawn in every mode, not just chat: switching
to a panel used to take all the chrome off screen.

Three things came out of porting it rather than from the branch:

- The one-row status bar STAYS. #175 removed it on the grounds that the
  rail carries the same four things, but its rail never grew the
  breadcrumb, so removing the bar left no indicator of where you are.
  The bar keeps the breadcrumb and drops its own brand lockup while the
  rail is up, since two copies read as a rendering bug.
- SIDEBAR_CHROME_ROWS goes 5 -> 13. The rail spends those rows on the
  mark, the version line and the Menu button. Measured off the rendered
  component, not estimated: at 24 rows the old constant had the rail
  drawing 28 rows into a 24-row terminal, and Ink 7 overlaps rather than
  clips, which garbles the whole frame.
- main's layout.ts is kept over #175's. It has SIDEBAR_OUTER_ROWS and
  SIDEBAR_MIN_ROWS, which #175 predates; only the constant is retuned.

Theme gains railBackground / railForeground / railMuted (additive; no
palette key is removed) and logo.tsx gains RAIL_MARK, the same drawing
as the splash rasterised to a 6x4 cell.

Tests: 4830 total, no new failures. The layout and smoke expectations
that encoded the old chrome are updated, not deleted: the row-budget test
still pins the 2:1 ratio, and the Tab-focus test still branches on
whether the rail is up.
Third slice of the #175 UI round.

The menu was anchored to the bottom of the pane, hanging off the prompt
like a dropdown. It is now a true overlay: position=absolute, centred on
both axes, with the app behind it dimmed through setBackdropDimmed.

availableColumns subtracts the rail width, so the frame is centred on the
content area rather than on the terminal — without that the right border
falls off screen whenever the rail is up.

The menu keeps this tree's onActivate contract: a node that carries a
slash name is run as that command, so the menu still has exactly one
dispatch path shared with the slash handler.

Tests: 4830 total, no new failures.
Fourth slice of the #175 UI round.

The prompt was an opencode-style left tail: one border column capped by a
`╹` glyph. It is now a closed rounded frame whose bottom row is an action
bar on the rail ground, carrying the model label and a clickable
`send →`. Net one row shorter than the tail, so CHROME_ROWS is unchanged.

The action bar is unconditional now. It used to appear only when a model
or a slot was set; since it carries Send, it cannot come and go without
the composer changing height under the operator.

Nothing here is coupled to the fusion stack. `formatModel` splits on a
local ` ⇄ ` constant so a two-legged label stays readable, but with no
fusion present the split yields one element and it behaves exactly as
before — kept so the fusion stack can land later without re-touching this
file.

Also corrects the debug-pane comment that still described the `╹` cap.

Tests: 4840 total, no new failures. prompt-shell.test.tsx is replaced
rather than merged: it asserted the tail cap the frame removes.
@sosidudku1
sosidudku1 merged commit 598d154 into main Aug 20, 2026
@sosidudku1
sosidudku1 deleted the feat/tui-ui-round branch August 20, 2026 12:27
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