Skip to content

fix(web): collapse the resting composer without replacing it - #7855

Open
chrisdeeming wants to merge 6 commits into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height-v2
Open

fix(web): collapse the resting composer without replacing it#7855
chrisdeeming wants to merge 6 commits into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height-v2

Conversation

@chrisdeeming

@chrisdeeming chrisdeeming commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Keep one mounted editor and primary-action tree while switching the idle desktop composer to a 50px single-line layout.
  • Collapse existing-thread composers on blur or a meaningful timeline scroll while the prompt editor is focused, then expand again on composer interaction.
  • Animate measured height changes over 180ms, including interrupted transitions, while respecting reduced-motion preferences.
  • Preserve the full composer for new threads, mobile layouts, attachments, menus, drawers, inline accessories, validation errors, and blocking environment states.
  • Preserve keyboard focus on composer controls and clicks on timeline actions such as Scroll to end.

Why

The resting composer occupies more vertical space than it needs. Compacting the existing controls reclaims that space without introducing duplicate editor state or focus handoffs, while scroll collapse creates room when reading longer threads.

UI Changes

Before

Empty composer

Empty composer before

Parked draft

Parked draft before

After

Empty composer

Empty composer after

Parked draft

Parked draft after

Video

browser-recording-mt4ye3nq.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Built with GPT-5.6-Sol via Codex in T3 Code.


Note

Medium Risk
Complex focus, scroll-collapse, portal, and height-animation logic in the chat composer can cause layout or focus regressions, but it is UI-only with no auth or data-path changes.

Overview
Idle desktop composers now compact to a single-line layout instead of keeping the full editor chrome. The same prompt editor and send actions stay mounted; only layout and control placement change.

On existing threads, the composer collapses on blur or a meaningful timeline wheel while the prompt is focused, and expands again on typing, pointer interaction, or focus. Height changes animate (~180ms) and honor reduced motion.

Model/mode/traits controls move into the branch context strip when resting (with overflow into the compact menu). Full height is kept for mobile, attachments, drawers/menus, inline badges, errors, and blocking env states. Toolbar controls are restyled to match the quieter strip.

Reviewed by Cursor Bugbot for commit c806486. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add desktop resting composer layout that collapses without replacing the composer

  • On desktop, the composer now collapses into a compact 'resting' layout when unfocused and unobstructed, instead of swapping in a replacement element; controls portal into a host in BranchToolbar and height animates via the Web Animations API
  • shouldUseRestingComposerLayout in composerFooterLayout.ts gates the layout based on focus, attachments, expanded chrome, and inline accessories
  • useRestingComposerControlsLayout measures available host width and hides trailing control blocks behind an overflow menu when space is constrained
  • Wheel gestures over the timeline (tagged via data-chat-messages-timeline) auto-collapse the composer after a small threshold; key/pointer/focus interactions expand it back
  • Toolbar trigger components (ProviderModelPicker, TraitsPicker, BranchToolbarBranchSelector, BranchToolbarEnvModeSelector, BranchToolbarEnvironmentSelector, MobileRunContextSelector) switch from font-medium to font-normal and support a compact xs size
  • Risk: ChatComposer portals resting controls into props.restingControlsHost; if the host ref is not provided by ChatView, resting controls will not render. useLabelsOverflow in BranchToolbar.tsx excludes the host from width calculations — any new toolbar children must avoid matching data-chat-resting-composer-controls-host

Macroscope summarized c806486.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 208dcece-a4ed-4da8-af72-f1b57f8d32b7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 21, 2026
@github-actions github-actions Bot added the size:L 100-499 changed lines (additions + deletions). label Aug 21, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the resting-composer layout against the shared composer/editor contracts. Two concrete regressions in the changed behavior are noted inline: the resting footer overlay can overlap the prompt line, and the new desktop pointerdown reconciliation can collapse the composer while the caret is still inside it.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated
Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeeming marked this pull request as ready for review August 21, 2026 23:55

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding on the resting layout: the reserved right gutter is padding, not a clip, so a long resting line can still paint under the absolutely positioned footer. The earlier inline-badge and pointerdown concerns look addressed (hasInlineAccessories, and pointerdown now defers to scheduleComposerCollapseCheck).

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Macroscope could not determine whether this PR modifies its approvability configuration, so the PR was not approved automatically. A PR that may change the rules that govern approval is never approved automatically.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Comment thread apps/web/src/components/chat/ChatComposer.tsx
Comment thread apps/web/src/components/chat/ChatComposer.tsx

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the resting/scroll-collapse composer changes for UI consistency and interaction preservation. Two concrete issues in apps/web/src/components/chat/ChatComposer.tsx; the earlier clipping concern (prompt text painting under the resting send button) looks resolved now that pr-12 sits on the positioned wrapper rather than the editor, and hasInlineAccessories keeps the inline tasks/stash badges out of the resting overlay.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated
Comment thread apps/web/src/components/chat/ChatComposer.tsx
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 23, 2026
environmentUnavailable !== null ||
composerSubmissionError !== null ||
providerInputSubmissionError !== null;
const isComposerResting = shouldUseRestingComposerLayout({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium chat/ChatComposer.tsx:2602

In an idle desktop thread without the branch context strip, restingControlsHost is null while isComposerResting still becomes true, so the footer hides composerControls and the model, runtime-mode, and traits controls disappear. Gate the resting state on restingControlsHost (or mount the host independently) so controls remain rendered until they have a portal destination.

Also found in 1 other location(s)

apps/web/src/components/ChatView.tsx:6676

restingControlsHost is always passed to ChatComposer, but its host is only mounted inside BranchToolbar when showComposerContextStrip is true. For an existing idle desktop thread in a non-Git project with no environment indicator, showComposerContextStrip is false while shouldUseRestingComposerLayout can still return true. In that state ChatComposer hides composerControls from its footer, but cannot portal them because restingControlsHost is null, so the provider/model, runtime-mode, and traits controls disappear. Either prevent resting without a host or mount the host independently of the context strip.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 2602:

In an idle desktop thread without the branch context strip, `restingControlsHost` is `null` while `isComposerResting` still becomes `true`, so the footer hides `composerControls` and the model, runtime-mode, and traits controls disappear. Gate the resting state on `restingControlsHost` (or mount the host independently) so controls remain rendered until they have a portal destination.

Also found in 1 other location(s):
- apps/web/src/components/ChatView.tsx:6676 -- `restingControlsHost` is always passed to `ChatComposer`, but its host is only mounted inside `BranchToolbar` when `showComposerContextStrip` is true. For an existing idle desktop thread in a non-Git project with no environment indicator, `showComposerContextStrip` is false while `shouldUseRestingComposerLayout` can still return true. In that state `ChatComposer` hides `composerControls` from its footer, but cannot portal them because `restingControlsHost` is `null`, so the provider/model, runtime-mode, and traits controls disappear. Either prevent resting without a host or mount the host independently of the context strip.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c806486. Configure here.

</div>,
restingControlsHost,
)
: null}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resting controls vanish without host

High Severity

When the composer is resting, footer model/traits/mode controls are hidden and only reappear via a portal into restingControlsHost. That host only exists when BranchToolbar mounts under showComposerContextStrip. If the strip is absent (non-git primary-only projects, or toolbar returns null), resting hides those controls with nowhere to render them.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c806486. Configure here.

resetAccumulatedScroll();
};
}, [canScrollCollapseComposer]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Picker remounts on resting open

Medium Severity

Opening the model picker from the resting strip sets isComposerModelPickerOpen, which clears resting and moves composerControls from the toolbar portal back into the footer. That fully remounts ProviderModelPicker under a new trigger while open is already true, so the popover jumps from the strip to the expanding footer (and can flicker or briefly lose its anchor).

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c806486. Configure here.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review of the latest commit (c806486, "keep resting composer controls inline"). Two issues where the resting layout can leave the composer with no visible provider/model, plan-build, or runtime controls, plus one shared-primitive default change that also affects the non-resting composer. Earlier findings on the scroll-collapse focus loss and the stale transition height look addressed.

Posted via Macroscope — UI Consistency

ref={composerControlsHostRef}
data-composer-context-control
data-chat-resting-composer-controls-host="true"
className="hidden min-w-0 flex-1 items-center justify-start overflow-hidden empty:hidden md:flex"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The host is visible from md up, but the composer's resting layout starts at sm: BranchToolbar uses useIsMobile() (max-md) for its mobile row, while ChatComposer derives isMobileViewport from useMediaQuery("max-sm").

Between 640px and 767px the composer therefore rests, the inline footer controls get hidden, and this portal target is display: none — the model picker, plan/build toggle and runtime select are neither visible nor reachable (the measurement hook also bails out on clientWidth === 0). That window is easy to hit in a narrow Electron window or split view.

Consider making the two breakpoints agree: gate the resting layout on max-md as well, or expose this host (and a strip layout that fits it) from sm up.

Posted via Macroscope — UI Consistency

<div
className={cn(
"-m-1 -ms-3.5 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 ps-3.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
isComposerResting && "hidden",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When restingControlsHost is null the resting layout still hides the inline footer controls here (and renders null at line 3853) while the portal at line 3367 renders nothing — so the model picker, plan/build toggle and runtime-mode select disappear from the UI entirely until the user clicks into the composer.

This is reachable: BranchToolbar is the only host and it is gated by showComposerContextStrip, i.e. hasActiveProject && (isGitRepo || showEnvironmentIndicator), which is false for a non-git project folder with a single primary environment.

Consider feeding host availability into the resting decision (e.g. an extra hasControlsHost: restingControlsHost !== null input to shouldUseRestingComposerLayout) so the composer only rests when it has somewhere to put its controls.

Posted via Macroscope — UI Consistency


const composerControlClassName =
"h-7 min-h-7 gap-1.5 rounded-[var(--control-radius)] px-2.5 text-secondary-label transition-none hover:text-foreground [&_svg[data-composer-control-icon]]:mx-0 [&_svg[data-composer-control-chevron]]:-mx-0.5";
"rounded-[var(--control-radius)] text-secondary-label transition-none hover:text-foreground [&_svg[data-composer-control-icon]]:mx-0 [&_svg[data-composer-control-chevron]]:-mx-0.5";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping h-7 min-h-7 px-2.5 hands geometry back to buttonVariants, but the default here is size="sm" = h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7. Below sm every existing composer control (model picker, plan/build toggle, runtime select) now renders 32px tall instead of 28px, so the mobile footer this PR otherwise preserves changes height.

Same pattern for ComposerControlChevron (lines 45-51): the new size-3 + opacity-50 default also applies to the expanded composer, replacing the semantic text-icon-muted token with an opacity hack, while the newly added className param is never passed by any caller.

Consider keeping the previous defaults and opting into the smaller/muted resting treatment per size — e.g. size === "xs" ? undefined : "h-7 min-h-7" inside ComposerControl/ComposerSelectControl, and passing the resting chevron tone through the new className prop.

Posted via Macroscope — UI Consistency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants