Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@
those imports to make a conflict go away, because nothing in the fork's
copy references them any more.

Both rows carry a trailing inset of --app-scrollbar-width. They sit in a
container with the same 8px padding as the thread list below, so their
content edges ought to agree — but the list is a scroll container and its
scrollbar takes real layout width, leaving every card short by exactly that
much and the trailing button overhanging them. The inset reads the token
rather than restating 6px, so the alignment follows upstream if the
scrollbar is ever retuned.

The brand moves to the header's trailing edge, where it stops competing
with the toggle for the corner the eye starts at. The search and project
rows are 36px and 12px rather than 32px and 14px so they read as chrome
Expand Down Expand Up @@ -426,6 +434,10 @@
- apps/web/src/components/SidebarStageBackdrop.test.tsx
- apps/web/src/components/SidebarV2.tsx
- apps/web/src/components/sidebar/SidebarChrome.tsx
# Defines --app-scrollbar-width, which the chrome rows' trailing inset reads
# to stay flush with the thread cards. Retiring or renaming that token
# silently un-aligns the two.
- apps/web/src/index.css
verify:
- apps/web/src/__fork_guards__/forkSidebarChrome.test.ts

Expand Down Expand Up @@ -646,3 +658,165 @@
- vite.config.ts
verify:
- apps/web/src/__fork_guards__/forkLintCleanliness.test.ts

- id: fork-composer-shell
intent: >
The chat composer is a discrete bordered box with its run controls floating
on a line underneath it, and it comes in two sizes. The new-chat screen gets
the tall shell: the prompt on its own line above a row carrying the model
and effort pills on the left and send on the right, in a 20px box. A started
thread gets the slim shell: one 48px line, prompt inline with those same
pills and send, in a 12px box. Slim grows into tall the moment the prompt
wraps past its first line, and never shrinks back within a turn the user is
still typing — the wrap is observed on the editor element, because Lexical
reflows without a React render.
Which controls sit where is the whole point, so it is worth stating plainly.
Inside the box: the model pill and the effort pill, and the send/stop
button. Everything else is on the control row below it — runtime mode
("Auto"), interaction mode ("Build"), the plan-sidebar toggle, the context
meter, the preparing-worktree line, the no-provider badge, and, at the
row's right end, the worktree/branch pair. Upstream splits that last pair
off into BranchToolbar rendered as a sibling of the composer; here it is
passed into ChatComposer as contextStrip and rendered as the control row's
right-hand group, which is why ChatView no longer renders it directly.
The largest thing this replaces is not a layout but a paint. Upstream draws
composer and branch strip as one continuous "glass shell": a single frosted
::before and a single 1px ::after, clipped by a shape() path that joins the
two through a notch, so they read as one vessel. The designs draw them
apart. Both pseudo-elements are switched off under the fork marker and the
box paints its own fill and border instead. The backdrop blur moves with it
rather than being dropped — the drawn fill is 80% opaque and only works over
a blurred backdrop. Focus is drawn by recolouring that border rather than
adding a ring, so the box keeps its 1px weight in both states and nothing
reflows when the caret lands.
Upstream's min-h-17.5 on the prompt editor is removed for the same reason:
it holds the editor open at roughly three lines, which makes the slim shell
geometrically impossible and leaves the tall one hollow. The prompt is
14px/16px at >=sm; below sm it stays 16px, because anything smaller makes
iOS zoom the viewport on focus. The designs draw the prompt at 12px — 14 is
the fork's call, matching the pills' own bump away from the drawn 10px.
The prompt and its placeholder are set by one rule and must stay that way.
They are two separate elements stacked exactly on top of each other, so any
disagreement about size or leading shows up as a caret sitting beside the
text it is supposed to be inside, and as text that changes size on the first
keystroke. The selector uses a general sibling combinator: Lexical renders
the placeholder as the editor's third child with an empty spacer between
them, so `+ div` silently misses it — which is exactly how that bug got in
the first time.
The line box stays 16px regardless of the type size, because the drawn box
heights are derived from it (96 tall, 48 slim). Scaling leading with the
font would push the tall box off its drawn height.
Density has three values, not two. `collapsed` is upstream's mobile tap
target, which the fork restyles but does not lay out, and it is deliberately
not a flavour of `slim`: folding it in put
data-fork-composer-density="slim" on a composer the slim layout was never
applied to, forced every call site to re-exclude the collapsed case by hand,
and left two things named "slim" disagreeing about what they meant.
Both short shells still take the 12px radius, which is why the CSS lists
them together rather than relying on one standing in for the other.
The wrap observer and its latch state live in custom/, beside the pure rule
they drive, rather than inline in ChatComposer. The split version put half a
rule in the file least able to absorb fences and made the latch reachable
only through a regex over the source.
The latch clears on a draft change as well as on an empty prompt.
ChatComposer carries no key in ChatView, so it survives thread switches and
so does the latch; without the draft reset, leaving a thread whose prompt
had wrapped and arriving at one with a short saved draft rendered the new
thread tall and stranded it there, because the prompt was non-empty and the
observer only ever latches on. "Only an empty prompt turns it off" is a rule
about one composing session, not about the component's whole lifetime.
contextStrip is passed as a memoised element, and that is correctness
rather than tuning. ChatComposer is memo'd and every other prop it takes is
a stable reference or a primitive, so an inline <BranchToolbar/> at the call
site is the single new object identity per render and defeats the memo
outright — on a ~2900-line component that re-renders throughout a streaming
turn. onStartFromOriginChange became a useCallback for the same reason: it
feeds that memo and was a bare arrow.
The box's fill and hairline are selected through data-fork-composer-surface,
not through `[data-fork-composer-box] > div`. The positional form held only
because the frame has exactly one child today; upstream adding a drop
overlay or a banner inside it would paint a second frosted card with its own
hairline, and every guard here would stay green, because they assert on the
text of the stylesheet rather than on what it matches. That gap is the
general weakness of this guard style and is worth remembering for the next
one.
The context meter and the worktree status line are pinned shrink-0 on the
control row. That row's left slot is overflow-x-auto with its scrollbar
hidden, so anything allowed to be squeezed out of it vanishes with no
affordance saying so. The mode controls may scroll — they have a compact
menu fallback — but the meter is a readout people watch to decide when to
compact, and silently hiding it is a different failure from crowding it.
The slim shell is desktop-only. At phone widths the model pill and the send
button leave the flex-1 editor 90-150px, and the placeholder — an absolutely
positioned overlay outside the editor's scroll box — wraps to several lines
and paints straight through the 48px box with nothing to clip it. It is also
stable rather than transient: the overlay never changes the observed editor
height, so the wrap latch cannot rescue it. A narrow viewport therefore
resolves tall regardless of everything else.
The control row renders in every density, including collapsed; only its left
half is conditional. Upstream showed BranchToolbar while the composer was
collapsed, gated on showComposerContextStrip alone and with its own mobile
selector, so env mode and branch were switchable without focusing the
composer and raising the keyboard. It also unmounted the whole footer during
a pending approval, which is why the run controls are gated on that here —
they moved out of the box, so they would otherwise stay live and let the
user flip modes for a run whose approval is unresolved. BranchToolbar is
deliberately not gated on approval; upstream showed it too.
Three rules had to be narrowed after they were found reaching further than
intended, and the common cause is worth stating once: this stylesheet is
unlayered while Tailwind v4 utilities sit in @layer utilities, so a fork
rule beats them regardless of specificity. The pill restyle is scoped to
data-fork-composer-pills rather than the footer wholesale, because the
footer also holds ComposerPrimaryActions' real CTAs — the Implement/Refine
split button and the pending-question Previous/Next/Submit set — which it
was squashing to 24px pills and, in dark, painting #a6a6a6 on a filled
primary background. The separator rule is scoped to the left slot, because
BranchToolbar draws its own separator in the right one. And the drag-over
state is repainted explicitly, because upstream signals it with
bg-accent/45 plus a ring on the very element whose background and
box-shadow this file pins — the ring being a box-shadow too, so a drag over
the composer had no feedback at all.
States the designs do not draw resolve to tall, because the slim shell is a
single 24px line with nowhere to put a second row of controls: an approval
request, a pending user-input question, and the plan follow-up banner all
own the box's internals and carry their own action rows. Attachments
deliberately do not force tall — that was considered and declined — so
image, annotation, review-comment and element-context cards render as a
full-width band above the inline row rather than as flex items inside it.
The pills are 12px, where the designs draw them at 10px. That is a
deliberate divergence, not a transcription slip: 10px reads as fine print
against a 12px prompt sitting an inch away, and these are controls the user
is meant to read at a glance. The guard pins 12px so a later reconciliation
against Figma does not quietly "correct" it back.
ComposerFooterPrimaryActions keeps rendering the context meter and the
worktree status line for any caller that wants them; the fork passes
showStatusAdornments={false} and renders both on the control row instead, so
the behaviour is relocated rather than dropped. At compact widths the effort
pill folds into CompactComposerControlsMenu exactly as upstream does, which
leaves the box carrying the model pill alone — intended, not a gap.
tier: 4
files:
- apps/web/src/custom/ComposerControlRow.tsx
- apps/web/src/custom/composerDensity.ts
- apps/web/src/custom/useComposerPromptWrapLatch.ts
- apps/web/src/theme.custom.css
shadows: []
watch:
# Carries the density derivation, the two shells, the control row, and the
# contextStrip prop.
- apps/web/src/components/chat/ChatComposer.tsx
# Passes BranchToolbar in as contextStrip instead of rendering it as a
# sibling of the composer.
- apps/web/src/components/ChatView.tsx
# data-fork-composer-action on the send and stop buttons; the whole 24px
# square restyle hangs off it.
- apps/web/src/components/chat/ComposerPrimaryActions.tsx
# The glass-shell pseudo-elements and .chat-composer-context-strip this
# customization switches off. An upstream rework of that paint would leave
# the fork disabling rules that no longer exist.
- apps/web/src/index.css
# min-h-17.5 on the prompt editor, and the placeholder that has to track the
# prompt's type ramp.
- apps/web/src/components/ComposerPromptEditor.tsx
verify:
- apps/web/src/__fork_guards__/forkComposerShell.test.ts
Loading
Loading