Skip to content

fix(ui-web): keep the composer bar to one row and let its labels shrink instead - #721

Merged
LivXue merged 1 commit into
mainfrom
fix/composer_bar_min_width
Sep 23, 2026
Merged

LivXue merged 1 commit into
mainfrom
fix/composer_bar_min_width

Conversation

@LivXue

@LivXue LivXue commented Sep 23, 2026

Copy link
Copy Markdown
Member

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:

  • Equal shares rather than proportional shrinking. Shrinking in
    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.
  • The chip shrinks with the anchor it hangs in, and only its name
    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 .under rule and
loses its flex-wrap too. Its bar never wrapped, because its message
line is already flex: 1, and all 12 of its layouts measured are
identical before and after.

Not fixed here:

  • The permission chip carries no hover title, so a shortened mode name
    is read in full only through its accessible name or its popover. The
    folder and model chips already carry one.
  • At the tightest combination (780px window, rail at its widest,
    context ring and usage meter shown) each label keeps about 5px, so
    those chips read as their icons.
  • Only Chrome was measured. The stylesheet already uses :has() in 28
    places, so this adds no browser requirement.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

In ui-web/, with NODE_OPTIONS=--localstorage-file=<file> so that
node 26 does not shadow happy-dom's localStorage. Everything below
ran on the final base, cfe082bb0 (#710), unless it says otherwise.

  • Red then green on the four new cases in 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, and shares what the controls leave equally, each at most its own width. With the fix, the file passes 26 of 26. The fourth
    case, keeps the controls that are not text at their own size, is a
    guard and passes on both.
  • Two mutants, each caught by exactly one case: widening the anchor
    selector so it also takes the "+" fails keeps the controls that are not text at their own size, and going back to proportional shrinking
    fails shares what the controls leave equally, each at most its own width.
  • Every test file that reads src/styles/page.css, plus the CJK
    literal gate: 20 files, 474 tests passed (with --testTimeout=30000,
    for the load noted below).
  • npx vitest run --no-file-parallelism on an earlier base,
    3a463d6cd: 206 files, 2858 passed, 4 failed, 1 unhandled error.
    None of them is this change's:
    • features/desk/palette.test.ts and features/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 session
      switch": 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's ui rpc contract job is green on
      that base.
    • an unhandled ReferenceError: document is not defined from a timer
      in state/detail.ts that fired after its test environment was torn
      down, attributed to features/extAgents/ExtAgentsPage.test.tsx. It
      does 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-deps in files this branch does not
    touch. npm run gen:check: matches the contract.
  • npm run build then python3 build.py: both boot goldens match (290
    and 289 nodes). check-page.mjs, check-css.mjs and
    check-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...HEAD and
pre-commit run --from-ref origin/main --to-ref HEAD all exit 0, and
no 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 with
and 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:

layouts measured base this change
English, 21, from 780px to 1440px 15 wrap 0 wrap
Chinese, 10, from 780px to 1440px 3 wrap 0 wrap
usage meter shown, 900 / 940 / 980px 2 wrap 0 wrap
where the old bar fit, 8 - 0 of 37 boxes move
sub-agent composer bar, 12 - identical
3 popovers over shortened chips - anchored, in view

The same measurements on the first base, 3a463d6cd, gave the same
results.

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

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 .under rule is shared with the sub-agent composer, which loses
flex-wrap: wrap too. Its layouts measured identical, since its
message line never let it wrap.

No security surface: stylesheet rules and one test file.

Rollback: revert the commit.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

…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>
@LivXue
LivXue requested review from 0xKT and gloryfromca September 23, 2026 09:34

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@LivXue
LivXue merged commit 0a40648 into main Sep 23, 2026
23 checks passed
@LivXue
LivXue deleted the fix/composer_bar_min_width branch September 23, 2026 09:50
0xKT pushed a commit that referenced this pull request Sep 23, 2026
…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>
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.

3 participants