fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(web): collapse the composer when it is not focused - #7446

Closed
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height
Closed

fix(web): collapse the composer when it is not focused#7446
chrisdeeming wants to merge 1 commit into
pingdotgg:mainfrom
chrisdeeming:fix/composer-resting-height

Conversation

@chrisdeeming

@chrisdeemingchrisdeeming commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What Changed

The composer collapses to a single ~50px row whenever it is not focused, instead of always reserving 144px.

  • A parked draft stays visible in that row, muted and truncated.
  • Staged attachments keep it expanded, since the row renders text only.
  • Narrow viewports already did this; the width gate is gone, so web, desktop and mobile now share one rule.

Why

The resting composer reserved a 70px minimum editor that never shrank when empty, plus a 48px toolbar row. That chrome stayed mounted while you were only reading, giving T3 Code a noticeably smaller transcript than comparable apps. Collapsing on blur returns ~94px to the thread.

UI Changes

Same thread at 1440x900, unfocused.

Before

Empty composer, then with a parked draft. Both 144px with the full toolbar.

imageimage

After

Empty composer, then with a parked draft. Both 50px; the draft shows muted and truncated.

imageimage

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No video this time

Written by Claude Opus 5 via Claude Code.


Note

Medium Risk
Large interaction surface in ChatComposer (focus, blur, collapse, popups, stash, imperative focus APIs); regressions could affect typing, sending, and keyboard shortcuts across web/desktop/mobile.

Overview
The chat composer now collapses to a single resting row when unfocused on desktop as well as mobile, not only on narrow viewports. Parked draft text still shows truncated in that row; attachments, in-flight image compression, running turns (Stop/spinner), plan follow-up chrome, and transient UI (stash menu, model picker, stash pulse) keep the full composer open via new shouldCollapseRestingComposer in composerFooterLayout.ts.

Focus and expand behavior is reworked: expandComposerThenFocus retries editor focus across animation frames and reconciles collapse if focus never lands; blur into any Base UI floating layer (dialogs, sheets, menus, etc.) defers collapse and focusin/pointerdown listeners re-run the check when focus leaves popups. Stash (⌘S) and restore paths avoid stealing focus or flashing expand when the editor is disabled.

A shared ComposerSendButton (with composerSendButtonLabel) unifies send styling, busy spinner, and aria labels in the expanded footer and collapsed row; the collapsed send can submit without expanding the composer.

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

Note

Collapse the ChatComposer when it loses focus across all viewports

  • Generalizes the composer collapse/expand logic from mobile-only to all viewports, driven by focus, attachments, busy state, and transient/actionable chrome via the new shouldCollapseRestingComposer utility in composerFooterLayout.ts
  • Defers collapse while focus is inside any floating popup (dialog, sheet, command menu, etc.) by expanding FLOATING_LAYER_SELECTOR and the isInsideFloatingLayer check in ChatComposer.tsx; a global focusin/pointerdown capture re-runs the deferred check once focus settles outside all popups
  • expandComposerThenFocus retries editor focus up to 5 animation frames and skips expansion when the editor is disabled, avoiding premature collapse
  • Introduces a shared ComposerSendButton component in ComposerSendButton.tsx with correct busy indicator, backdrop styling, and accessible labeling; replaces the inline send button in ComposerPrimaryActions.tsx
  • Behavioral Change: data-chat-composer-mobile-collapsed is replaced by data-chat-composer-collapsed; in-flight image compressions now count as attachments for collapse gating, and stash-by-shortcut restores focus to the pre-stash element if it was outside the composer

Macroscope summarized 49d0019.

@coderabbitai

coderabbitaiBot commented Aug 18, 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: 7f12ae3a-1591-4289-bc54-682cc3fd83a0

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@macroscopeappmacroscopeappBot 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.

Two findings on the resting-composer change. Extending the collapsed state from mobile to every viewport is fine in principle, but the editor is hidden with display:none while it stays mounted, and nothing in the imperative focus paths un-collapses it — so desktop focus entry points silently stop working. The collapsed draft text also now uses a token that is identical to the placeholder token in the shipped palettes.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@macroscopeapp

macroscopeappBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR extends mobile composer collapse behavior to desktop, introducing new user-facing functionality with complex focus/blur reconciliation logic. Multiple review comments identified bugs in the implementation that required fixes. The scope and complexity warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 164e54b to 690627cCompareAugust 18, 2026 23:39
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 690627c to 01ebc6bCompareAugust 18, 2026 23:48

@macroscopeappmacroscopeappBot 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.

Collapse now applies on desktop, so keyboard-driven composer surfaces that only exist inside the collapsed (hidden / unmounted) subtree lose their entry point. Two spots below.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 01ebc6b to c84ebfdCompareAugust 18, 2026 23:51

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The focus entry points now route through expandComposerThenFocus, which resolves the earlier "collapsed forever on desktop" problem. Two findings remain in the collapsed row, which is now the default resting state on desktop rather than a mobile-only view.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: one issue found in the resting-composer collapse work. The earlier findings (menus opening against an unmounted subtree, and Tab entry into the collapsed row) look correctly addressed by hasOpenMenu and the onFocus/data-chat-composer-collapsed-controls split.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from c84ebfd to 3e02768CompareAugust 19, 2026 00:05
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 3e02768 to 6f1ab32CompareAugust 19, 2026 00:12

@macroscopeappmacroscopeappBot 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 desktop resting-composer change. The previously flagged issues (focus entry expanding the editor, keyboard Tab entry, draft vs. placeholder tone, sm:px-4 alignment, keybinding-opened menus and the stash pulse holding the composer open) all look resolved, and every editor focus entry point now routes through expandComposerThenFocus. One minor desktop affordance gap remains on the collapsed row.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

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.

Now that this row is the resting state on desktop, hovering the composer shows the default arrow cursor: Tailwind v4's preflight sets no cursor on button, and the shared controls opt in explicitly (Button uses cursor-pointer, ComposerPrimaryActions uses enabled:cursor-pointer). The expanded composer shows a text caret over the same area, so the cursor flips on focus. cursor-text here keeps the resting row reading as an input.

Suggested change
"min-w-0 flex-1 cursor-text truncate rounded-sm bg-transparent p-0 text-left text-[14px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 6f1ab32 to f80fc34CompareAugust 19, 2026 00:31
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeappmacroscopeappBot 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 collapse for UI consistency. The earlier gaps (imperative focus entry points, tab-in, menu/pulse hold-open, sm:px-4, draft vs placeholder tone, focus ring) all look resolved. Two remaining items are specific to the collapsed row now being the desktop resting state, where it was previously mobile-only: it drops the composer's typography settings, and its buttons have no pointer cursor.

Posted via Macroscope — UI Consistency

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch 2 times, most recently from 647ad5f to b1744d3CompareAugust 19, 2026 00:55
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from b1744d3 to 4ce8dffCompareAugust 19, 2026 01:03
Comment threadapps/web/src/components/chat/ChatComposer.tsx

@macroscopeappmacroscopeappBot 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: the new isBusy term keeps the composer permanently expanded while an approval or pending user-input request is outstanding, which makes the collapsed approval / pending-input variants below unreachable and changes existing mobile behavior. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +1062 to +1064
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase === "running" || isSendBusy || isConnecting || isPreparingWorktree,

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.

isBusy folds in phase === "running", but the session status stays running for the whole time an approval or user-input request is outstanding — the server never dispatches a session change for request.opened / user-input.requested (and a provider waiting state is mapped to running), and derivePhase reads nothing but session.status. So whenever activePendingApproval or pendingUserInputs is non-empty, shouldCollapseRestingComposer returns false.

That contradicts the comment just above ("Approvals and pending inputs … render their own collapsed variants") and makes those variants dead: isComposerCollapsed && activePendingApproval (2870) and isComposerCollapsed && pendingUserInputs.length > 0 (2887) can no longer render. Those rows shipped on mobile before this PR, where the old rule (isMobileViewport && !forceExpandedOnMobile && !isComposerFocused) had no busy term — so a narrow viewport now keeps the full approval panel plus editor plus toolbar mounted at rest instead of the compact row, and showMobilePendingAnswerActions (1300) is pinned on for the same reason.

Excluding outstanding requests from the busy gate keeps the Stop/spinner rule for real in-flight turns while restoring the collapsed request rows:

Suggested change
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label.
isBusy: phase==="running"||isSendBusy||isConnecting||isPreparingWorktree,
// Every in-flight signal, not just a running turn: the footer owns the
// Stop control, the send spinner and the "Preparing worktree" label. A
// pending request is excluded: the session stays "running" while it waits,
// and it has its own collapsed row carrying the actions.
isBusy:
(phase==="running"||isSendBusy||isConnecting||isPreparingWorktree)&&
activePendingApproval===null&&
pendingUserInputs.length===0,

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This was the best catch of the review — fixed in 09638f168 with your suggestion.

I verified the chain rather than taking it on trust: orchestrationSessionStatusFromRuntimeState maps waiting to running (ProviderRuntimeIngestion.ts:273-291), and derivePhase reads nothing but session.status (session-logic.ts:1710-1722). So phase really does stay running for the whole time a request is outstanding, and my isBusy term made the collapsed approval and pending-input rows unreachable — rows that shipped on mobile before this PR. That is a regression I introduced, and the comment above the flag was wrong as a result.

isBusy now excludes outstanding requests, so real in-flight turns still keep Stop and the spinner mounted while those variants render again.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 4ce8dff to 09638f1CompareAugust 19, 2026 01:32
Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 09638f1 to a1d9c55CompareAugust 19, 2026 02:03

@macroscopeappmacroscopeappBot 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 consistency gap: the collapsed pending-user-input row now renders on desktop but did not get the pointer/focus treatment the sibling collapsed prompt row received in this PR.

Posted via Macroscope — UI Consistency

Comment on lines 2940 to 3012

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.

This collapsed "Write custom answer" row is now a desktop resting state too (gate at 2911 is isComposerCollapsed, and isBusy excludes pending inputs), but it kept the mobile-only styling while the sibling collapsed prompt row below gained cursor-pointer and a focus-visible ring. On a pointer device the row highlights on hover (hover:bg-background/80 on the wrapper) yet shows the default arrow — and in the multi-select branch the ComposerPrimaryActions button next to it carries enabled:cursor-pointer, so the cursor flips mid-row. Suggest matching the other collapsed expander:

- "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm",+ "min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 64abe7ebd as suggested — the "Write custom answer" row now carries cursor-pointer, rounded-sm and the focus-visible ring, matching the sibling collapsed expander. The cursor no longer flips mid-row against the adjacent ComposerPrimaryActions button.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment threadapps/web/src/components/chat/ChatComposer.tsx Outdated
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from a1d9c55 to 64abe7eCompareAugust 19, 2026 02:29

@macroscopeappmacroscopeappBot 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: the collapsed pending-answer row is now a desktop resting state but still renders composer prompt text in the interface font, unlike the sibling collapsed prompt row that was aligned to the editor's typography tokens.

Posted via Macroscope — UI Consistency

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.

This row now renders at rest on desktop too (gate at 2946 is isComposerCollapsed), and it shows activePendingProgress.customAnswer — the exact value the editor renders (3241). The editor wrapper types that text with the appearance tokens ([font-family:var(--font-composer,…)] [font-size:var(--font-size-prompt,0.875rem)], ComposerPromptEditor.tsx:1751), which the Prompt font / Prompt font size settings write (12–20px, optional custom family). Hardcoding text-sm in the interface font means a parked custom answer is shown at the wrong size/family and re-typesets on expand for anyone who changed those settings — the same contract the sibling collapsed prompt row below now honours (3015). Suggest using the same tokens here.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent py-1.5 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",

Posted via Macroscope — UI Consistency

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 64abe7e to 61d1cbeCompareAugust 19, 2026 02:53

@macroscopeappmacroscopeappBot 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 collapsed prompt row: it stays interactive in a state where the editor cannot take focus, so the new expand/reconcile handshake flashes the composer open and shut.

Posted via Macroscope — UI Consistency

Comment on lines 3015 to 3025
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event) => event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

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 projectSelectionRequired is set (local draft with no project), the editor is rendered disabled (line 3271), so Lexical's root is contenteditable="false" and focusAt's rootElement.focus() is a no-op. Clicking this row now expands the composer, misses focus on all five attempts, and the release frame reconciles by collapsing it again — on desktop the resting composer flashes open and shuts and can't be opened at all (previously it just stayed expanded, and collapse never applied here outside mobile).

The sibling send button already opts out of that state through collapsedComposerPrimaryActionDisabled; disabling this button too keeps the row consistent with the disabled editor and with the opacity-75 the surface already shows.

Suggested change
"min-w-0 flex-1 cursor-pointer truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandMobileComposer}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"
"min-w-0 flex-1 truncate rounded-sm bg-transparent p-0 text-left [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)] enabled:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-placeholder",
)}
disabled={projectSelectionRequired}
onPointerDown={(event)=>event.preventDefault()}
onClick={expandComposer}
// Tabbing here is a request to start typing, so expand and let
// the caret land in the editor rather than parking on a button.
onFocus={expandComposer}
aria-label="Expand composer"

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Correct, and this was a regression from the retry loop I added for the previous finding — it turned "stays expanded" into "flashes open and shut", which is worse. Fixed in db1b04e2f.

Two layers, since the click path is not the only way in:

  1. The expand button is now disabled under the same three conditions that disable the editor (isConnecting || isComposerApprovalState || projectSelectionRequired), with enabled:cursor-pointer so the affordance matches. That mirrors how the sibling send button already opts out.
  2. expandComposerThenFocus bails before starting the handshake when the editor is disabled. There are six entry points into it (imperative handle, type-to-focus, prompt replacement, stash restore, context insertion), and any of them could otherwise hit the same flash — the guard covers all of them, not just the click.

Verified the happy path is unaffected: on a normal thread the row is enabled and still expands 50px → 144px with the caret landing in the editor.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 61d1cbe to db1b04eCompareAugust 19, 2026 03:02
Comment threadapps/web/src/components/chat/ChatComposer.tsx
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from db1b04e to ae79b5fCompareAugust 19, 2026 03:13

@macroscopeappmacroscopeappBot 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 consistency finding on the collapsed (resting) composer row, which this PR promotes from a mobile-only state to the desktop resting state.

Posted via Macroscope — UI Consistency

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.

Now that the resting row is a desktop state, this hand-rolled send button occupies the same slot as the shared one but does not look like it. The send control in ComposerPrimaryActions.tsx:222-268 is the same 32px circle on desktop, yet it carries shadow-xs + enabled:shadow-message-action/24, enabled:inset-shadow-[0_1px_--theme(--color-white/16%)], hover:scale-105 and active: feedback, and draws a 14px glyph at strokeWidth="1.8"; this copy is flat with a 16px glyph at strokeWidth="2" (line 3075). Clicking the row swaps one for the other in place, so on desktop the send affordance visibly changes weight and gains a shadow/hover-scale the moment the composer expands. The aria-label also stays "Send message" where the shared button reports sendDisabledReason / "Environment disconnected".

Suggest lifting the shared send button's visual (classes + glyph) into one small component and rendering it here too, instead of maintaining a second divergent copy.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, and took the suggested route rather than patching the copy — fixed in 52f0e825f.

New ComposerSendButton component; both the expanded footer and the collapsed resting row render it, so there is no second divergent copy to drift. The label is derived by a shared composerSendButtonLabel() helper, so the collapsed button now reports sendDisabledReason / "Environment disconnected" / "Connecting" / "Preparing worktree" / "Sending" instead of a static "Send message". Extracting it also left Spinner, StageBackdropButtonArt, useSidebarStageBackdropVariant and useEnvironmentIdentificationMode unused in ComposerPrimaryActions, which are now removed.

Measured both states in the browser at 1440px and they are byte-identical: 32x32, 14px glyph at stroke-width 1.8, 0.15s transition, same shadow and same label. Clicking the row no longer changes the send affordance.

Note the two electronPasskeys.test.ts typecheck errors on this branch are pre-existing — I confirmed they reproduce on a clean upstream/main checkout.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from ae79b5f to 52f0e82CompareAugust 19, 2026 23:43
@github-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026

@cursorcursorBot 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 1 potential issue.

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 52f0e825ff2663926e8bcc0e24dfed28b5e80440. Configure here.

Comment threadapps/web/src/components/chat/ChatComposer.tsx
The resting composer reserved 144px on every thread: a 70px minimum
editor that never shrank when empty, plus a 48px toolbar row. That
chrome stayed mounted while you were only reading, leaving T3 Code with
a noticeably smaller transcript than comparable apps.
Collapse it to a single ~50px row whenever it is unfocused. A drafted
message stays visible there, muted and truncated, and staged
attachments keep the composer open since the row renders text only.
Narrow viewports already behaved this way; this drops the width gate so
web, desktop and mobile share one rule.
@chrisdeeming
chrisdeemingforce-pushed the fix/composer-resting-height branch from 52f0e82 to 49d0019CompareAugust 19, 2026 23:51
@chrisdeeming

Copy link
Copy Markdown
ContributorAuthor

Superseded by #7855.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chrisdeeming