Skip to content

Center the composer on a fresh chat - #2452

Open
gary149 wants to merge 4 commits into
mainfrom
claude/centered-input-redesign-mg91dq
Open

Center the composer on a fresh chat#2452
gary149 wants to merge 4 commits into
mainfrom
claude/centered-input-redesign-mg91dq

Conversation

@gary149

@gary149 gary149 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Before the first message the composer no longer sits at the bottom of an
empty column: it centers vertically with the app greeting above it, and
docks to the bottom as soon as the conversation has content — the layout
every other chat app uses for its new-chat screen.

  • ChatWindow: the composer overlay switches between bottom-0 (docked)
    and bottom-1/2 translate-y-1/2 (centered). Pure CSS, so server-rendered
    markup already lands centered and nothing jumps on hydration.
  • max-h-full + justify-end clamp the centered box to the column height,
    so a group taller than the viewport (long draft plus attachments on a
    short screen) falls back to the docked position instead of pushing the
    send button below the fold.
  • The greeting moved out of the scroll column and above the composer;
    clientHeight is now bound to an inner wrapper so the greeting can never
    inflate the clearance the scroll spacer derives from it.
  • Touch devices dock while the input is focused: the virtual keyboard
    covers the lower half of the screen and fires no ResizeObserver, so a
    centered composer would end up behind it.
  • "Fresh" counts rendered messages only, so a conversation created but
    never sent to — it still carries an invisible system preprompt message
    — gets the same screen instead of a blank column, and the example chips
    and caveat line show there too.

Verified in Chromium (desktop 1280x800/520, tablet, iPhone 13 portrait and
landscape, 320x568) against both dev and production builds: centering is
exact in every viewport, docking is unchanged after a send, and the
keyboard-focus/blur, attachment, drag-drop and dropdown paths all behave.
469 unit tests and 32 e2e specs pass.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_011s35VQavGEgfwSAR15kuok

claude added 2 commits July 25, 2026 14:08
Before the first message the composer no longer sits at the bottom of an
empty column: it centers vertically with the app greeting above it, and
docks to the bottom as soon as the conversation has content — the layout
every other chat app uses for its new-chat screen.

- ChatWindow: the composer overlay switches between `bottom-0` (docked)
  and `bottom-1/2 translate-y-1/2` (centered). Pure CSS, so server-rendered
  markup already lands centered and nothing jumps on hydration.
- `max-h-full` + `justify-end` clamp the centered box to the column height,
  so a group taller than the viewport (long draft plus attachments on a
  short screen) falls back to the docked position instead of pushing the
  send button below the fold.
- The greeting moved out of the scroll column and above the composer;
  clientHeight is now bound to an inner wrapper so the greeting can never
  inflate the clearance the scroll spacer derives from it.
- Touch devices dock while the input is focused: the virtual keyboard
  covers the lower half of the screen and fires no ResizeObserver, so a
  centered composer would end up behind it.
- "Fresh" counts rendered messages only, so a conversation created but
  never sent to — it still carries an invisible `system` preprompt message
  — gets the same screen instead of a blank column, and the example chips
  and caveat line show there too.

Verified in Chromium (desktop 1280x800/520, tablet, iPhone 13 portrait and
landscape, 320x568) against both dev and production builds: centering is
exact in every viewport, docking is unchanged after a send, and the
keyboard-focus/blur, attachment, drag-drop and dropdown paths all behave.
469 unit tests and 32 e2e specs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s35VQavGEgfwSAR15kuok
The starter chips above a fresh composer were mounted and unmounted, and
inside a vertically centered group that moved everything: measured at
1280x800, the input jumped 18px up and the greeting 18px down on the first
keystroke, and the same 18px the other way when the MCP store hydrated and
the row appeared — a visible pop on every load of the new-chat screen.

Both rows now split "does this row belong on this screen" from "should its
chips be showing", keeping the box and fading only the chips. The chips stay
rendered and hidden with `invisible`, so the reserved height is the row's
real height rather than a hardcoded one, and hidden chips leave the tab
order and the accessibility tree.

For the hydration half, the row's presence has to be knowable during SSR:
`mcpServersLoaded` is false there, so it reads the base-server list out of
the SSR payload — base servers are enabled unless the user turned them off —
rather than guessing. A deployment with no MCP servers configured reserves
nothing, so no dead space appears where the row will never show.

Verified in Chromium against a dev server in both configurations: with MCP
servers configured, the input top and greeting top are identical across SSR
paint, hydration, idle and typing (406 / 262); with MCP_SERVERS=[] nothing
is reserved and the geometry is unchanged from before (388 / 280). The
follow-up row behaves the same after a first exchange. 469 unit tests and
32 e2e specs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s35VQavGEgfwSAR15kuok

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5be62461ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

);
let toolExamplesApply = $derived(
currentModel.isRouter ||
(modelSupportsTools && ($mcpServersLoaded ? $allBaseServersEnabled : baseMcpServerCount > 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve row geometry for disabled MCP preferences

When a returning user has disabled one of the configured base MCP servers, the SSR branch still treats baseMcpServerCount > 0 as though all servers were enabled and reserves the hidden examples row. During hydration, initWithServers restores the disabled IDs from localStorage, $allBaseServersEnabled becomes false, and this expression removes the row for a fresh non-router tool model, visibly shifting the newly centered composer. Avoid changing the reservation solely when the client-side disabled-server preference hydrates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 9793147. Reproduced at 1280x800 by writing a configured base server's id into chatui:mcp:disabled-base-ids: input top 406 at SSR paint, 388 after hydration.

Worth noting it's wider than "disabled servers" — allBaseServersEnabled requires every base server to be selected, so turning off a single one is enough to trigger it.

The reservation now reads only the configured base-server list from the SSR payload, which server and client agree on, and $allBaseServersEnabled moves to the chips-visibility condition where the client is the authority. The trade is that a user in this state keeps an empty reserved row on a non-router tools model — invisible whitespace in a centered layout, where a jump on every page load is not.

Verified across SSR paint, hydration and first keystroke in three configurations: base servers enabled (chips show, 406/262 throughout), every base server disabled (row reserved and empty, 406/262 throughout), and MCP_SERVERS=[] (nothing reserved, 388/280 throughout).


Generated by Claude Code

claude added 2 commits July 27, 2026 09:38
Reserving on `$allBaseServersEnabled` put the row back in the hands of a
client-only preference. A returning user who has turned off any one
configured base MCP server — `allBaseServersEnabled` requires every one of
them, so a single toggle is enough — got the row reserved during SSR and
dropped once `initWithServers` restored the disabled ids from localStorage:
on a non-router tools model the composer moved 18px at hydration, exactly
the shift this split exists to remove. Reproduced at 1280x800, input top
406 -> 388.

The reservation now reads only the configured base-server list from the SSR
payload, which the server and the client agree on, and `$allBaseServersEnabled`
moves to the chips-visibility condition where the client is the authority.
Such a user keeps an empty reserved row on a non-router tools model, which
is invisible whitespace in a centered layout — a jump on every page load is
not.

Verified against a dev server in three configurations, checking SSR paint,
hydration and first keystroke: base servers enabled (chips show, 406/262
throughout), every base server disabled (row reserved and empty, 406/262
throughout), and MCP_SERVERS=[] (nothing reserved, 388/280 throughout).
469 unit tests pass.

Reported-by: chatgpt-codex-connector in PR review
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s35VQavGEgfwSAR15kuok
Three things still moved after the server-rendered markup appeared, all
because the composer's layout was reading values the server does not have.
Measured on a production build at 1280x800, sampling every frame from
navigation:

- The input jumped 10px right and narrowed by 20px. `--scrollbar-gutter` can
  only be measured from the live scroll container, so SSR emits 0 and
  hydration writes the real half-gutter into the composer's padding. It is
  held at zero while the composer is centered: there are no messages to align
  with yet, and when the composer docks the compensation rides along with a
  several-hundred-pixel vertical move.
- Every chip's label changed — "HTML game" to "Generate an image" — because
  the starter set was chosen by `$allBaseServersEnabled`, a store that only
  fills in at hydration. The set now comes from the configured base-server
  list in the SSR payload, the same fact the server already has.
- The chips faded in ~380ms after first paint, gated on `$mcpServersLoaded`.
  Throttled to 1.6Mbps they were still invisible 4.2s in, which is what
  "sometimes the examples show, sometimes not" was: they wait for the bundle.
  The gate is gone, so the chips ship visible in the server HTML.

The MCP store's one unique fact is which servers the user has since switched
off, held in localStorage. That is deliberately no longer consulted here —
honouring it would put first paint back in the hands of a value the server
cannot see. A user who turned MCP off now sees tool-flavoured starters, which
are prompts they can send either way.

Before: 4 distinct layout states between first paint and settled, or 2 with
the chips never arriving when throttled. After: one state, fast and
throttled alike. Typing still hides the chips without moving the composer,
docking still aligns composer text with message text (365 vs 362), and the
disabled-server and no-MCP deployments are stable across hydration too.
469 unit tests and 32 e2e specs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s35VQavGEgfwSAR15kuok
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.

2 participants