Skip to content

feat(web): redesign chat composer to the new Figma layout - #41

Merged
NoahHendrickson merged 4 commits into
customfrom
t3code/redesign-chat-composer-layout
Aug 1, 2026
Merged

feat(web): redesign chat composer to the new Figma layout#41
NoahHendrickson merged 4 commits into
customfrom
t3code/redesign-chat-composer-layout

Conversation

@NoahHendrickson

Copy link
Copy Markdown
Owner

Problem

The chat composer didn't match the updated t3-fork Figma (node 71-4121): an earlier attempt got the bones right but deviated on type sizes, chip treatments, and control styling, and still carried the old tall/slim density machinery.

Fix

Reworked the fork-composer-shell customization to the current design plus adjustments made during live review:

  • Context row — Current checkout + branch as filled #2a2a2a chips, 24px tall, 4px radius, 4px inline padding (2px optical correction on the folder glyph), no hairline, workspace caret dropped.
  • Prompt surface — 40px base row (8px padding around the 24px action), body/md 14px/23px type, full muted-foreground placeholder, muted #474747 focus recolour, send button pinned top-right as the prompt wraps.
  • Control row — 20px controls at 12px medium type with 4px-radius hover fills. Runtime mode is a per-mode accent chip (auto green, full access yellow, auto-accept edits purple, supervised red) with label only; Build/Plan toggles are text-only; model + effort selects share the row type, and the effort label joins traits with a space ("High 1M").
  • Context meter — flex-centred wrapper, 16px ring art in a 24px hit target, exempt from the row's control sizing.
  • DeletedcomposerDensity.ts, useComposerPromptWrapLatch.ts, and the density switch; the surface now grows only with the editor.

Manifest intent, watch list (TraitsPicker.tsx added), and the guard suite updated to assert the new outcomes — 178 guard tests, web typecheck, and .fork/lint-owned.mjs all pass.

Before/after screenshots to follow in a comment.

Built with Claude Fable 5 on Claude Code.

🤖 Generated with Claude Code

The composer now matches the updated t3-fork Figma (node 71-4121) plus
Noey's live-review adjustments: filled 24px context chips with the Auto
chip's 4px-radius container, a 40px prompt surface with body/md type and
a full muted-foreground placeholder, a 20px control row with per-mode
accent chips (auto green, full access yellow, auto-accept edits purple,
supervised red), text-only mode toggles, square 4px hover fills, a
centred context-window ring, and the send button pinned top-right as the
prompt wraps. The old tall/slim density machinery and wrap latch are
deleted; the effort trigger drops upstream's dot separator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Jul 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thermo-nuclear code quality — request changes

Net deletion is good, and killing the wrap latch / tall-slim machinery is real simplification. That does not clear the bar: this still deepens last-resort fenced edits in a 3.3k upstream file, leaves a density concept that no longer earns its keep, and pushes more attribute/exclusion-chain CSS into an already-over-1k theme file.

Must address before approve

  1. Stop growing fork-composer-shell fences in ChatComposer — extract a custom/ shell (or accept full ownership via override) so layout composition lives outside the hot upstream file.
  2. Delete the density attribute/"base" leftover entirely; keep collapsed as a boolean/mobile concern only.
  3. Replace mode-chip / control-geometry styling that grows via :not() exclusion chains with component-owned styles or a typed mode→token map.
  4. Scope the TraitsPicker label join so Settings does not silently inherit a composer-only design call.
  5. Drop or justify ComposerContextRow; fix the intent (20px) vs CSS (24px) drift.

Also: stale comment ~2678–2680 still claims model/effort pills live inside the box beside send — contradicts the new layout. Delete it. ComposerControlRowProps.right JSDoc still describes worktree/branch.

Net-negative lines are not the same as structural cleanup. The code-judo move is still available: one shell module in custom/, almost no fences left in ChatComposer.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

data-chat-composer-form="true"
>
{/* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */}
{contextStrip ? <ComposerContextRow>{contextStrip}</ComposerContextRow> : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is still last-resort fencing inside a 3.3k hot file — and this PR adds more of it. .fork/AGENTS.md preference order is custom/** → override → theme → fences. the code-judo move: extract a custom/ComposerShell (context slot + surface slot + control row) and leave ChatComposer with one small fence that renders that shell. do not keep sprinkling fork-composer-shell through imports, FooterModeControls, density, placeholder, and layout. shadowing the whole composer is only viable if you intend to own it; extracting the shell is cleaner than either more fences or a full 3.3k override.

isNarrowViewport: isMobileViewport,
});
const isComposerSlim = composerDensity === "slim";
const composerDensity = isComposerCollapsedMobile ? "collapsed" : "base";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

density is dead as a model and should be deleted, not renamed. "base" is never styled; only "collapsed" changes radius. this PR killed tall/slim/latch correctly, then preserved a vestigial attr + ternary + guard pin for a concept that no longer exists. use a boolean (data-fork-composer-collapsed) or style the existing mobile collapsed path directly — no density enum, no "base" string, no guard asserting the leftover.

Comment thread apps/web/src/theme.custom.css Outdated
:is(button, [data-slot="button"]):not([data-fork-composer-action]) {
height: 24px;
min-height: 24px;
:is(button, [data-slot="button"]):not([data-fork-composer-action]):not(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this exclusion chain is the wrong growth model. every new control type becomes another :not(...) on a shared geometry selector, and the dark color rule already drifts (status excluded from geometry but not from the dark label color). prefer component-owned classes / CSS variables on the controls themselves so the shared rule does not need to know every exception by name.

// whole restyle and its value picks the mode's accent color;
// the popup keeps its icons and descriptions.
<ComposerSelectControl
data-fork-composer-mode-chip={props.runtimeMode}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

putting the whole accent restyle on data-fork-composer-mode-chip={runtimeMode} forces ~80 lines of near-duplicate CSS mode blocks and the exclusion-chain tax above. better: a tiny custom mode-chip wrapper (or a typed mode→token map applied as classes/vars) that owns height, radius, wash, and hover. attribute soup in theme.custom.css is not a design system.

/* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
/* A plain space, not upstream's " · ": the composer's effort chip reads
"High 1M". Settings uses the same trigger and stays consistent. */
return { label: labels.join(" "), showFastModeIcon: fastModeEnabled };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is a shared helper; Settings uses the same trigger. a composer-only typography call (" · "" ") should not silently restyle Settings. pass a join/separator (or a display override) from the composer call site, or keep upstream join and format only in the composer shell.

}

/** Repository and worktree controls that sit above the prompt surface. */
export const ComposerContextRow = memo(function ComposerContextRow({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ComposerContextRow is a memoized div + data-* + pb-2. that does not earn a named export, a prop interface, or a guard that pins the wrapper. put data-fork-composer-context-row on the call-site element (or fold into ComposerShell) and delete the identity component.

Comment thread apps/web/src/theme.custom.css Outdated
hue; light mode keeps the wash and drops each label to its dark Tailwind
shade so it stays readable. Hover deepens the wash rather than falling
back to the ghost variant's gray. */
:root[data-fork="noahhendrickson-t3code"] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

theme.custom.css was already over 1k and this PR adds ~200 more lines of composer attribute CSS. if mode tokens stay in CSS, extract the composer block into a dedicated fork stylesheet (or co-locate with the shell module) instead of letting the global theme file keep absorbing layout. also: four nearly-identical mode hue blocks should collapse to a token table, not copy-paste.

Comment thread .fork/customizations.yaml Outdated
Which controls sit where is the main design contract. BranchToolbar's
worktree and branch chips sit above the surface as filled chips sharing
one selected-surface fill with no hairline, 4px apart, on the Auto chip's
container at the drawn 4px inline padding (20px tall, 4px radius) at 12px

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

intent still says context chips are "20px tall"; CSS comment + rules implement 24px ("four pixels taller (Noey's call)"). pick one number and make intent, CSS, and guards agree — drift here is how the next sync "fixes" the wrong thing.

Copy link
Copy Markdown
Owner Author

Code review — feat(web): redesign chat composer to the new Figma layout

I pulled the branch, read the full diff against custom, traced the removed states through ChatComposer.tsx, and checked the result against Figma node 71-4121 directly. The layout work is genuinely good — deleting composerDensity.ts and useComposerPromptWrapLatch.ts removes a resize-observer/latch machine that existed only to service a two-shell design that no longer exists, and the diff is net −475 lines. That's the right direction.

But the verification claim in the description ("178 guard tests, web typecheck, and .fork/lint-owned.mjs all pass") is scoped to fork-owned code, and the change reaches outside it. Three of the findings below are things that scope can't see.


1. 🔴 Blocking — this breaks TraitsPicker.test.ts

buildTraitsTriggerDisplay switched from labels.join(" · ") to labels.join(" "). That function has an upstream unit test that pins the separator, and it is not in the guard suite you ran:

apps/web/src/components/chat/TraitsPicker.test.ts

  • L50 — label: "High · 1M"
  • L57 — label: "High · 1M"
  • L90 — label: "High · Thinking On"

Three assertions fail on the current head. I could not execute vitest here (workspace deps aren't installed in this environment), but the assertions are literal string equality against the exact value the diff changed — there's no ambiguity. Please run vp test run apps/web/src/components/chat/TraitsPicker.test.ts and update it as part of this PR.

2. 🔴 The model and effort selects stay live during an unresolved approval

This is the regression the code you deleted was specifically written to prevent. The old comment said it out loud:

During a pending approval, upstream replaced the entire footer with ComposerPendingApprovalActions, unmounting the runtime-mode, Build/Plan and plan-sidebar toggles. […] they moved out here, so they need the same gate — otherwise the user can flip modes for a run whose approval is still unresolved.

The new control row gates only the left half:

<ComposerControlRow
  left={activePendingApproval ? null : composerModeControls}
  right={composerModelAndStatusControls}
/>

right now carries the model picker and the effort/traits picker, which upstream unmounted along with the rest of the footer during an approval. The reasoning that justified gating the mode controls applies verbatim to the model and effort selects — changing the model mid-approval is arguably worse than changing the runtime mode, since it retargets the provider for a turn already in flight. right needs the same gate (the context meter can stay — it's a readout, not a control).

3. 🔴 Two primary-action clusters on mobile during a pending question

The old footer carried showMobilePendingAnswerActions && "hidden sm:flex", which suppressed composerPrimaryActionSlot on phones whenever the absolutely-positioned data-chat-composer-mobile-pending-actions block was rendering. The new inline slot has no such gate:

{!activePendingApproval ? (
  <div data-chat-composer-inline-actions="true" className="flex shrink-0 items-center self-start">
    {composerPrimaryActionSlot}
  </div>
) : null}

With isMobileViewport && !isComposerCollapsedMobile && pendingPrimaryAction !== null, both render: the inline cluster pinned top-right by self-start, and the absolute one bottom-right in the 44px the editor still reserves via max-sm:pb-11. Two Next/Submit affordances for one question. The gate needs to come back.


4. 🟡 The traits separator change leaks into Settings

The fence comment acknowledges this and waves it through:

A plain space, not upstream's " · ": the composer's effort chip reads "High 1M". Settings uses the same trigger and stays consistent.

"Consistent" is doing a lot of work. "High 1M" reads fine because 1M is unambiguously a second token. "High Thinking On" — the case the upstream test covers — does not; the separator was carrying the token boundary. The design asks for this in the composer only. Consider passing a separator option (or a compact flag) rather than changing the shared function's behavior everywhere, so the composer gets the drawn treatment and Settings keeps its legibility.

5. 🟡 Model/effort pills now render under the collapsed mobile composer

Previously the pills were reachable only in the tall footer or the slim inline row, both of which were skipped when composerDensity === "collapsed". The control row's right slot is now unconditional, so the meter, model and effort chips sit below the collapsed mobile tap target. That may be an improvement, but it isn't in the description or the manifest intent, and per CLAUDE.md a restyle shouldn't change what a component does as a side effect. Please state the intent either way.


6. 🟠 The guard suite got smaller and more brittle at the same time

This is the part I'd push back on hardest, because it's the mechanism the fork depends on to survive upstream syncs.

Coverage dropped from ~30 cases to 14, and the intent text still asserts things nothing guards any more:

Intent still claims Guard deleted
"The prompt wrapper owns the 12.5rem scroll cap" moves the desktop prompt scrollport onto the wrapper, not the editor
"explicit drag-over outline" repaints the drag-over state the surface rule would otherwise swallow
"the placeholder/spacer overflow containment remains in place" resizes the placeholder with the prompt, via a general sibling combinator
provider icon dropped in the composer only drops the provider icon from the model pill, in the composer only
surface selected by name paints the surface by name, so an added sibling cannot inherit the box
frees the prompt from upstream's three-line minimum, hides only the left slot's separators, takes the drawn colours in dark and defers to upstream in light

Those CSS rules all still exist in theme.custom.css. They're just unguarded now — which is exactly the state .fork/AGENTS.md says the guard suite exists to prevent ("an upstream sync that silently drops the customization turns CI red instead of passing quietly"). The scrollport and provider-icon-scoping guards in particular encode bugs that were already found once.

And the replacements assert source text, not outcomes. .fork/AGENTS.md is explicit that a guard asserts the outcome. These don't:

expect(chatComposer).toContain('className="flex min-w-0 items-center gap-6"');
expect(chatComposer).toContain('"relative p-2"');
expect(chatComposer).toMatch(
  /<ComposerControlRow\s+left=\{activePendingApproval \? null : composerModeControls\}\s+right=\{composerModelAndStatusControls\}/u,
);

That last one pins a local variable name and the exact prop order. Add one utility class, run vp fmt in a way that reflows the JSX, or rename composerModeControls — CI goes red with zero behavior change. Meanwhile findings #2 and #3 above sail straight through, and #2 is literally inside the string being matched. The guard reads the gate and asserts nothing about what the gate should cover. That's the worst of both: high false-positive rate, no true-positive coverage.

The CSS guards (cssRules-based, checking selectors and declarations) are the good pattern here and should be the model — assert the rendered contract, not the source that produces it.

7. 🟠 The manifest contradicts the CSS on chip height

.fork/customizations.yaml L1138:

…on the Auto chip's container at the drawn 4px inline padding (20px tall, 4px radius) at 12px type…

theme.custom.css says height: 24px / min-height: 24px, the PR body says "24px tall", and the guard's own comment says "the Auto chip's container four pixels taller — 24px". The manifest is the fork's source of truth for intent; it's the one place that's wrong.

8. 🔵 Stale prop doc in fork-owned code

ComposerControlRow.tsx L7–12 still documents right as:

Where the message runs: the worktree/branch context strip. Absent on a project with no git repo, which leaves the row left-aligned rather than collapsing it — the left controls still apply.

right is now the model/effort/meter group and is always passed, so both sentences are wrong. The right?: optional and the {right ? … : null} branch are also dead now — worth either restoring a real absent case or dropping the optionality.


On the placeholder — not a defect, but worth a decision

I checked node 71-4121 rather than assume. The design genuinely reads "Ask anything,", trailing comma and all, so the implementation is faithful and I'm not flagging it as a typo.

But it's a comma-terminated sentence fragment shipping to users, and it drops @/$//discoverability — including fromaria-placeholder`, since that's fed the same string, so screen-reader users lose it too. The fork previously cared about this enough to keep a shortened hint in the slim shell specifically so those three sigils stayed discoverable. Worth taking back to design as a copy bug rather than reproducing it pixel-faithfully.

Things I checked that are fine

  • --color-emerald-700 / -amber-700 / -violet-700 / -red-700 all resolve — Tailwind v4's default palette is available and index.css already uses them.
  • The [data-testid="composer-editor"] ~ div placeholder rule is safe: the only following siblings inside Lexical's wrapper are the empty <span/> spacer and the placeholder div; data-chat-composer-mobile-pending-actions is outside that container.
  • data-chat-composer-footer has no remaining references anywhere in the repo.
  • isComposerFooterCompact still measures correctly — it observes composerFormRef, not the removed footer element, so CompactComposerControlsMenu still works.
  • :not([data-fork-composer-status] button) raises the geometry rule to (0,4,2), comfortably above the mode-chip rule's (0,3,1). The exclusion-by-name comment is accurate.

Summary: #1 breaks CI, #2 and #3 are real behavioral regressions in states the redesign didn't intend to touch, and #6 removes the safety net that would have caught #2 on the next upstream sync. The layout and the density-machinery deletion are solid work — please gate right on approval, restore the mobile pending-answer suppression, fix or update the traits test, and rebuild the guards around outcomes.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Following up on finding #1 in my review above — CI has now completed on 602a12c8, so I can replace the caveat ("I could not execute vitest here") with the actual result.

Test job — failed. 3 failed / 1888 passed, 1 failed file / 213 passed.

All three failures are the ones predicted, in apps/web/src/components/chat/TraitsPicker.test.ts:

FAIL  buildTraitsTriggerDisplay > omits fast mode from the label entirely when it is off
  expected { label: 'High 1M' } to deeply equal { label: 'High · 1M' }        TraitsPicker.test.ts:49

FAIL  buildTraitsTriggerDisplay > shows the bolt instead of a text label when fast mode is on
  expected { label: 'High 1M' } to deeply equal { label: 'High · 1M' }        TraitsPicker.test.ts:56

FAIL  buildTraitsTriggerDisplay > keeps non-fastMode booleans as text labels
  expected { label: 'High Thinking On' } to deeply equal { label: 'High · Thinking On' }
                                                                              TraitsPicker.test.ts:89

Two things worth drawing out of this:

It's the only thing failing. Check, Release Smoke, and Mobile Native Static Analysis are all green, so findings #2 and #3 (the approval gate and the duplicated mobile pending-answer actions) have no test coverage anywhere — which is the point made in #6. The suite cannot see either regression.

The third failure is the argument in finding #4, stated by the test itself. "High Thinking On" is exactly the ambiguity flagged: with the separator gone, the trait boundary disappears and two independent traits read as one four-word phrase. That case renders in Settings, where the design change doesn't apply. Please don't resolve this by editing the expected strings — the test is describing a real legibility loss. Scope the space-join to the composer's effort trigger and let buildTraitsTriggerDisplay keep " · " for its other caller.


Generated by Claude Code

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code review — request changes

Reviewed 602a12c8 against custom, the live CI log, and the fork rules in .fork/AGENTS.md. Net deletion of the tall/slim latch machinery is the right call. The layout direction is good. The blockers below are behavioral regressions and a red required check — not polish.

Blocking

  1. Required CI / Test is redTraitsPicker.test.ts (3 failures). buildTraitsTriggerDisplay changed labels.join(" · ")labels.join(" ") without updating or scoping the shared helper. Confirmed in job 91225628679. Do not “fix” this by rewriting the expected strings alone: Settings uses the same trigger (SettingsPanels.tsx), and "High Thinking On" loses the trait boundary the separator was carrying. Scope the space-join to the composer call site (separator option / display override), keep " · " as the shared default, and leave the upstream unit test green.

  2. Model + effort stay interactive during an unresolved approval. Relocating them onto ComposerControlRow.right dropped the gate that used to unmount them with the in-box footer. Only left is gated today:

left={activePendingApproval ? null : composerModeControls}
right={composerModelAndStatusControls}

Changing model/effort mid-approval retargets the provider for a turn already awaiting a decision. Gate the interactive right-side controls the same way; the context meter can stay as a readout.

  1. Duplicate primary-action clusters on mobile during a pending question. The old tall footer suppressed composerPrimaryActionSlot with showMobilePendingAnswerActions && "hidden sm:flex". The new inline slot only checks !activePendingApproval, so phones still render both the top-right inline cluster and the absolute data-chat-composer-mobile-pending-actions block (max-sm:pb-11). Restore the suppression.

High

  1. Guard suite regressed in the direction that matters. Coverage went ~30 → 14 cases, and several still-live CSS contracts (12.5rem scrollport, drag-over outline, placeholder overflow clip, composer-only provider-icon hide, surface-by-name paint) lost their pins while the intent text still claims them. Worse, the new guards assert source text / prop names / class strings — including the incomplete approval gate in finding feat(web): Sidebar V2 tab design, pixel-rain working indicator, Phosphor icons #2 — instead of outcomes. That is high false-positive, zero true-positive for the regressions above. Rebuild around cssRules + behavioral contracts.

  2. Collapsed mobile now exposes model/effort under the tap target. Previously those lived in the footer that unmounted while collapsed. Intent only documents keeping the branch strip reachable. Either gate right (minus meter) while collapsed, or explicitly record the new behavior in the manifest.

Medium

  1. Manifest vs CSS chip height drift: intent says context chips are (20px tall, 4px radius); CSS + PR body + guard comments implement 24px. Manifest is the fork source of truth — fix it to 24px (or change the CSS).

  2. Stale guidance in the hot file: ChatComposer.tsx ~2678 still says model/effort “live inside the box beside the send button.” ComposerControlRowProps.right JSDoc still describes the worktree/branch strip. Both will mislead the next sync.

Nits / already covered elsewhere

  • Thermo-nuclear’s structural notes (extract a custom/ComposerShell, delete vestigial density="base", collapse mode-chip CSS duplication) are valid maintainability debt, secondary to the blockers above.
  • Placeholder "Ask anything," matches Figma 71-4121; still worth a design copy check because it drops @/$// discoverability from aria-placeholder too.

Verdict

Request changes. Land the layout once (1) CI is green without weakening the shared traits contract, (2) approval + mobile pending gates are restored for the relocated controls, and (3) guards again pin the outcomes the intent claims — especially the gates this PR broke.

Reviewed by Cursor Cloud agent (run) — Cursor Grok 4.5.

flip modes for a run whose approval is still unresolved. BranchToolbar
is deliberately not gated on approval; upstream showed it too. */}
{/* Modes sit below-left; context usage, model and effort sit below-right. */}
<ComposerControlRow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking — approval gate incomplete after relocation. Model/effort used to unmount with the in-box footer during activePendingApproval. They now live on right, but only left is gated. Changing model/effort mid-approval retargets the provider for a turn still awaiting a decision. Gate the interactive right-side controls the same way; keep the context meter if you want a readout.

/* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
/* A plain space, not upstream's " · ": the composer's effort chip reads
"High 1M". Settings uses the same trigger and stays consistent. */
return { label: labels.join(" "), showFastModeIcon: fastModeEnabled };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking — CI red + Settings leak. This shared helper now joins with " ", which fails TraitsPicker.test.ts (3 cases) and also restyles Settings’ traits trigger. "High Thinking On" loses the trait boundary the " · " separator was carrying. Please don’t just update the expected strings — pass a composer-only separator/override and keep " · " as the default for other callers.

Comment thread .fork/customizations.yaml Outdated
Which controls sit where is the main design contract. BranchToolbar's
worktree and branch chips sit above the surface as filled chips sharing
one selected-surface fill with no hairline, 4px apart, on the Auto chip's
container at the drawn 4px inline padding (20px tall, 4px radius) at 12px

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium — intent/CSS drift. This says context chips are 20px tall; theme.custom.css implements 24px (“four pixels taller (Noey's call)”), and the PR body/guards agree with 24. Manifest is the fork source of truth — update this to 24px (or change the CSS).

hasComposerHeader, which resolves density to tall, so the
right conjunct can never be false when the left is true. */}
{isComposerSlim ? (
{!activePendingApproval ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking — duplicate mobile primary actions. When showMobilePendingAnswerActions is true, the absolute block below still mounts, and this inline slot no longer has the old hidden sm:flex suppression. Phones get two Next/Submit clusters for one question. Restore !showMobilePendingAnswerActions (or equivalent) here.

expect(chatComposer).toContain("<ComposerContextRow>{contextStrip}</ComposerContextRow>");
expect(chatComposer).toContain('data-fork-composer-box="true"');
expect(chatComposer).toMatch(
/<ComposerControlRow\s+left=\{activePendingApproval \? null : composerModeControls\}\s+right=\{composerModelAndStatusControls\}/u,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High — guard pins the incomplete gate as a string. This asserts the exact JSX that leaves right ungated during approval (finding above), so a real regression is locked in as “correct.” Prefer an outcome-level contract: interactive model/effort controls must not render while activePendingApproval is set.

@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Addressed the review feedback in 98a258c:

  • Extracted the context/surface/control composition into fork-owned ComposerShell, folded away ComposerContextRow, and removed the vestigial density attribute/model.
  • Gated mode, model, and effort controls during unresolved approvals and collapsed mobile while retaining read-only status; suppressed the inline primary action when mobile pending-answer actions are visible.
  • Restored the shared traits separator and added a composer-only separator option, so Settings keeps High · Thinking On while the composer can render High 1M.
  • Replaced the duplicated runtime-mode CSS blocks with a typed mode-to-token map and component-owned styling, and narrowed ghost geometry to named interactive groups.
  • Rebuilt the fork guard around rendered output and CSS rule outcomes, restoring coverage for the desktop scrollport, placeholder clipping, drag outline, named surface paint, composer-only provider icon, and left-slot separator scope.
  • Corrected the manifest to 24px context chips and updated the intent for approval, collapsed-mobile, and duplicate-action behavior.

Local verification: 199 focused tests passed (including all 181 fork guards), web typecheck passed, fork-owned lint reported no warnings, and targeted formatting passed.

@github-actions github-actions Bot added size:XXL and removed size:XL labels Jul 31, 2026
NoahHendrickson and others added 2 commits July 31, 2026 17:18
Empty-draft hero text was still stacked just above the input after the
composer was docked. Render the greeting as its own centered layer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@NoahHendrickson
NoahHendrickson merged commit 3561131 into custom Aug 1, 2026
10 checks passed
@NoahHendrickson
NoahHendrickson deleted the t3code/redesign-chat-composer-layout branch August 1, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant