Repository navigation
fix(ui-web): keep the composer bar to one row and let its labels shrink instead - #721
Conversation
…nk instead The bar under the composer field was a flex row allowed to wrap. A wrapping flex row picks its line breaks from its items' whole widths before any of them shrinks, so once the chips outgrew the column, the send button, last in the row, dropped onto a second line of its own: on main at every width from the 780px floor to 820px, and up to 940px once the usage meter shows text. The row no longer wraps. The four items whose width is their text (the folder, the permission mode, the usage meter and the model) each take an equal share of what the fixed controls leave, never more than their own width, and their labels end in an ellipsis. The anchor around a chip is not clipped, because the chip's popover stands outside it. Where the bar already fit, every box stays where it was. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
I reviewed the full diff and surrounding composer DOM/CSS, including the nested flex anchors, popover positioning, intrinsic-size constraints, the shared sub-agent .under row, and the existing meter/chip rules. I also checked the branch history and project rules in AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and ui-web/CONTEXT.md. The change preserves the chrome/island ownership boundary and introduces no compatibility concern beyond the already-established :has() baseline. Tests were added rather than weakened.
Verification: npm test -- --run src/chrome/Dock.test.tsx passed 26/26; npm run type-check passed; npm run lint passed with 0 errors and 4 pre-existing warnings in untouched files. GitHub's page checks and UI RPC contract are also green.
…reveal (#732) ## Summary Four fixes from one acceptance pass over the chat beside the desk pane and the settings dialog. - **Desk pane width.** A file opened at a fixed 960px, which on a laptop took nearly all of the split and left the composer too narrow to write in. It now opens at half of what the chat and the pane share (320px floor). Agent and task panes keep 440px. A width the reader dragged the column to outranks both: the grip already persisted it, but opening a pane overwrote it, so closing and reopening lost the choice. The stored width is read through a new `state/paneWidth.ts`, so the desk does not import the chrome (the import-direction gate holds). - **Composer bar.** On top of the one-row, equal-share bar from #721, the bar now gives items up whole once sharing runs out, measured on the dock container: the usage line and the folder/permission words first (icons stay), then the folder chip, the context ring and the permission chip. The plus, the model and send never go. - **Schedules.** Paused rows no longer repeat "paused"; the group heading already says it. - **About page (feat).** The config file and storage rows now open the host's file manager instead of copying the path: the config file selected, agent home opened. `fs.reveal` gains a `place` param (`config` or `workspace`) that the gateway resolves from its own config. The page sends no path, so the viewer's fence (which refuses the state directory and folders) is left as it was, and nothing is read back. `path` becomes optional in the contract. When the page is served from another machine the button still copies the path, since the reveal would open on the gateway's host. `hostIsLocal` moves to `lib/platform` so settings and workspace share one copy. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `npx vitest run` in ui-web: 209 files, 2892 tests passed - `npx tsc --noEmit`, `npx eslint src --quiet`, `npm run gen:check`, ui-tui `npm run lint:rpc`: clean - `npm run build`, `python3 ui-web/build.py`, `check-page.mjs`, `check-css.mjs`, `check-class-namespace.mjs`: OK - `uv run ruff check raven tests`, `uv run ruff format --check raven tests`: clean - `uv run pytest tests/test_rpc_console.py tests/test_rpc_schema_match.py tests/test_rpc_instances_declare_their_errors.py`: 548 passed - The new desk-width, remembered-width and paused-row tests each fail with their fix reverted - Real browser against a live gateway: file pane opened at 588 of a 1176 split, and at 600 with a stored 600; composer bar measured at eight dock widths from 828 to 198px, one row at every width, items dropping in the order above - Not exercised end to end: the About reveal, since the running gateway predates the `place` param; covered by the new pytest cases - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk User-visible: the desk pane opens narrower than before on wide screens, composer chips hide on narrow columns, and the About buttons open Finder / Explorer / the file manager. `fs.reveal` keeps its path behaviour and fence; the new `place` branch only reaches two gateway-resolved locations. Roll back by reverting the individual commit. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues #721 --------- Co-authored-by: arelchan <204152633+arelchan@users.noreply.github.com> Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
Summary
At the narrowest width the web UI lays out, the bar under the composer
field broke onto a second line and the send button dropped onto it,
left-aligned under the "+". The bar was a flex row allowed to wrap, and
a wrapping flex row picks its line breaks from its items' whole widths
before any of them shrinks. So a second line was the only answer it
had to a narrow column, and the send button, last in the row, was the
item that landed there.
Measured on main before this change, the range is wider than the 780px
case reported. With the rail open, the bar wrapped at every width from
the 780px floor to 820px (838px was the first width measured to fit),
and up to 940px once the usage meter shows text (980px fit). It also
wrapped at 780px in Chinese with the context ring shown, at 780px with
the rail at its widest (which leaves the chat column its 430px floor),
and at 1024px once a long folder name, a long model id, the context
ring and a populated meter shared the row.
The row no longer wraps, so no width can put the send button on a line
of its own. The four items whose width is their text (the folder, the
permission mode, the usage meter and the model) each take an equal
share of what the fixed controls leave, never more than their own
width, and each label ends in an ellipsis. Two decisions are worth
reviewing:
proportion takes the most from the widest box, and a chip is mostly
its icon and padding, so a short model name beside a long folder name
went to nothing. Capped at its own width, a short name stays whole
and only a long one gives: in Chinese at 780px with the context ring,
both 48px labels stay whole and only the model name shrinks.
clips. The anchor itself is not clipped, because the popover it holds
stands outside the anchor's box.
Where the bar already fit, nothing moves: all 37 boxes in it measured
identical before and after in the 8 layouts where the old bar fit
(860px to 1440px, with and without the context ring, long names and a
populated meter). The sub-agent composer shares the
.underrule andloses its
flex-wraptoo. Its bar never wrapped, because its messageline is already
flex: 1, and all 12 of its layouts measured areidentical before and after.
Not fixed here:
is read in full only through its accessible name or its popover. The
folder and model chips already carry one.
context ring and usage meter shown) each label keeps about 5px, so
those chips read as their icons.
:has()in 28places, so this adds no browser requirement.
Type
Verification
In
ui-web/, withNODE_OPTIONS=--localstorage-file=<file>so thatnode 26 does not shadow happy-dom's
localStorage. Everything belowran on the final base,
cfe082bb0(#710), unless it says otherwise.src/chrome/Dock.test.tsx.With the stylesheet at the base, three fail:
never wraps,lets every box between a label and the bar give, and ends the label in an ellipsis, andshares what the controls leave equally, each at most its own width. With the fix, the file passes 26 of 26. The fourthcase,
keeps the controls that are not text at their own size, is aguard and passes on both.
selector so it also takes the "+" fails
keeps the controls that are not text at their own size, and going back to proportional shrinkingfails
shares what the controls leave equally, each at most its own width.src/styles/page.css, plus the CJKliteral gate: 20 files, 474 tests passed (with
--testTimeout=30000,for the load noted below).
npx vitest run --no-file-parallelismon an earlier base,3a463d6cd: 206 files, 2858 passed, 4 failed, 1 unhandled error.None of them is this change's:
features/desk/palette.test.tsandfeatures/desk/store.test.ts,their "when storage refuses" cases: the known node 26 local
artifact, green in CI, failing the same way on the base.
state/session/registry.test.ts, cases of "the live sessionswitch": 5000ms timeouts on a box at load average 48 to 90 (two in
the full run, three with the file alone, the same on a tree
byte-equal to the base). The file passes 28 of 28 with
--testTimeout=30000, and CI'sui rpc contractjob is green onthat base.
ReferenceError: document is not definedfrom a timerin
state/detail.tsthat fired after its test environment was torndown, attributed to
features/extAgents/ExtAgentsPage.test.tsx. Itdoes not reproduce with that file alone, on either tree, and
neither file reads anything this change touches.
npm run type-check: clean.npm run lint: 0 errors, 4 warnings,all
react-hooks/exhaustive-depsin files this branch does nottouch.
npm run gen:check: matches the contract.npm run buildthenpython3 build.py: both boot goldens match (290and 289 nodes).
check-page.mjs,check-css.mjsandcheck-class-namespace.mjs: OK.Gates:
commitlint --from origin/main --to HEAD,scripts/check_commit_messages.py origin/main..HEAD,scripts/check_large_files.py origin/main...HEAD,scripts/check_source_language.py origin/main...HEADandpre-commit run --from-ref origin/main --to-ref HEADall exit 0, andno hook modified a file.
The suite has no layout engine, so the bar was also measured in
headless Chrome against the fixture transport (
?stub=1), built withand without the fix from the final base. Per layout the probe reads
whether every item in the bar shares one row, the send button's offset
from the bar's right edge, overlap between neighbours, and any icon
spilling out of its chip:
The same measurements on the first base,
3a463d6cd, gave the sameresults.
No user-facing doc describes the bar, so none changed.
Risk
At widths where the bar used to wrap, it now stays on one row and its
text labels shorten with an ellipsis instead; the send button stays at
the right end. Where the bar already fit, nothing moves.
The
.underrule is shared with the sub-agent composer, which losesflex-wrap: wraptoo. Its layouts measured identical, since itsmessage line never let it wrap.
No security surface: stylesheet rules and one test file.
Rollback: revert the commit.
Related Issues
N/A