From d44095a830b0940dc09329828483df2981f7c6ef Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 27 Jul 2026 18:37:00 -0400 Subject: [PATCH 1/4] fix(fork): make the release composer send button flat white Release builds were still using upstream primary purple; pin a pure white fill and black icon while Dev/Nightly keep channel art. Co-authored-by: Cursor --- .fork/customizations.yaml | 8 ++++++-- .../__fork_guards__/forkComposerShell.test.ts | 11 ++++++++++- .../components/chat/ComposerPrimaryActions.tsx | 7 ++++--- apps/web/src/theme.custom.css | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 2bf7078900c..ed644ff1311 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -615,7 +615,10 @@ the latch survive thread switches, and "only an empty prompt turns it off" is a rule about one composing session, not the component's lifetime. Which controls sit where is the whole point. Inside the box: the model - pill, the effort pill, and the send/stop button. Everything else is on the + pill, the effort pill, and the send/stop button. On a release build the send + button is flat pure white with a black icon + (data-fork-composer-send-tone="flat"); Dev and Nightly keep the + stage-channel fill. 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 @@ -711,7 +714,8 @@ # 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. + # square restyle hangs off it. Release send also carries + # data-fork-composer-send-tone="flat" for the white/black fill. - 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 diff --git a/apps/web/src/__fork_guards__/forkComposerShell.test.ts b/apps/web/src/__fork_guards__/forkComposerShell.test.ts index 540593c7396..9f85ebaef3d 100644 --- a/apps/web/src/__fork_guards__/forkComposerShell.test.ts +++ b/apps/web/src/__fork_guards__/forkComposerShell.test.ts @@ -182,8 +182,17 @@ describe("fork guard: fork-composer-shell", () => { expect(theme).toMatch(/\[data-testid="composer-editor"\][\s\S]{0,160}min-height:\s*0/u); }); - it("squares the send and stop buttons and reddens stop", () => { + it("squares the send and stop buttons, flattens release send, and reddens stop", () => { expect(theme).toMatch(/\[data-fork-composer-action\]\s*\{[\s\S]{0,200}border-radius:\s*6px/u); + // Release (tone=flat) is pure white / black icon — not upstream primary. + // Dev/Nightly keep tone=channel and the stage art; do not collapse them. + expect(primaryActions).toContain('data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"}'); + expect(theme).toMatch( + /\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}background:\s*#ffffff/u, + ); + expect(theme).toMatch( + /\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}color:\s*#000000/u, + ); expect(theme).toMatch( /\[data-fork-composer-action="stop"\][\s\S]{0,120}background:\s*#ea3150/u, ); diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index afeab513f60..5ee739c57e9 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -203,12 +203,13 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ type="submit" /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */ data-fork-composer-action="send" + data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"} /* fork:end fork-composer-shell */ className={cn( - "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full text-primary-foreground shadow-xs transition-all duration-150 enabled:cursor-pointer enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] hover:scale-105 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:pointer-events-none disabled:opacity-30 disabled:shadow-none disabled:hover:scale-100 sm:h-8 sm:w-8", + "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full transition-all duration-150 enabled:cursor-pointer hover:scale-105 disabled:pointer-events-none disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8", stageBackdropVariant - ? "bg-transparent enabled:shadow-black/24 enabled:hover:brightness-110" - : "bg-primary/90 enabled:shadow-primary/24 hover:bg-primary", + ? "bg-transparent text-primary-foreground shadow-xs enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] enabled:shadow-black/24 enabled:hover:brightness-110 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:shadow-none" + : "bg-white text-black shadow-none hover:bg-white active:shadow-none", )} {...pointerFocusProps} disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent} diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css index 56f52cb4f6a..d65282f7e19 100644 --- a/apps/web/src/theme.custom.css +++ b/apps/web/src/theme.custom.css @@ -434,6 +434,23 @@ border-radius: 6px; } +/* Release send: flat pure white with a black icon. Dev/Nightly keep the + stage-channel fill (tone="channel"); the attribute is set from + ComposerPrimaryActions so a missing art wrapper cannot silently leave the + release button on upstream's primary purple. */ +:root[data-fork="noahhendrickson-t3code"] + [data-fork-composer-action="send"][data-fork-composer-send-tone="flat"] { + background: #ffffff; + color: #000000; + box-shadow: none; +} + +:root[data-fork="noahhendrickson-t3code"] + [data-fork-composer-action="send"][data-fork-composer-send-tone="flat"]:hover:enabled { + background: #ffffff; + color: #000000; +} + :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-action="stop"] { background: #ea3150; } From 2b32f0e84cefe24e8f22822a5db3507e07032cc3 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 27 Jul 2026 18:43:41 -0400 Subject: [PATCH 2/4] style(fork): format release-send guard assertion Co-authored-by: Cursor --- apps/web/src/__fork_guards__/forkComposerShell.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/__fork_guards__/forkComposerShell.test.ts b/apps/web/src/__fork_guards__/forkComposerShell.test.ts index 9f85ebaef3d..3b0661df1e4 100644 --- a/apps/web/src/__fork_guards__/forkComposerShell.test.ts +++ b/apps/web/src/__fork_guards__/forkComposerShell.test.ts @@ -186,7 +186,9 @@ describe("fork guard: fork-composer-shell", () => { expect(theme).toMatch(/\[data-fork-composer-action\]\s*\{[\s\S]{0,200}border-radius:\s*6px/u); // Release (tone=flat) is pure white / black icon — not upstream primary. // Dev/Nightly keep tone=channel and the stage art; do not collapse them. - expect(primaryActions).toContain('data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"}'); + expect(primaryActions).toContain( + 'data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"}', + ); expect(theme).toMatch( /\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}background:\s*#ffffff/u, ); From 5c1118ce069ae7aabbf9055fabd520dbeeb6a3a1 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 27 Jul 2026 18:44:14 -0400 Subject: [PATCH 3/4] fix(fork): let CSS alone own the release send fill Drop the duplicate Tailwind white/black classes and the hover twin rule so attribute + unlayered CSS are the single paint owner. Co-authored-by: Cursor --- apps/web/src/components/chat/ComposerPrimaryActions.tsx | 2 +- apps/web/src/theme.custom.css | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index 5ee739c57e9..3c992a12515 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -209,7 +209,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full transition-all duration-150 enabled:cursor-pointer hover:scale-105 disabled:pointer-events-none disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8", stageBackdropVariant ? "bg-transparent text-primary-foreground shadow-xs enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] enabled:shadow-black/24 enabled:hover:brightness-110 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:shadow-none" - : "bg-white text-black shadow-none hover:bg-white active:shadow-none", + : "shadow-none", )} {...pointerFocusProps} disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent} diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css index d65282f7e19..dad9e7c02ff 100644 --- a/apps/web/src/theme.custom.css +++ b/apps/web/src/theme.custom.css @@ -437,7 +437,8 @@ /* Release send: flat pure white with a black icon. Dev/Nightly keep the stage-channel fill (tone="channel"); the attribute is set from ComposerPrimaryActions so a missing art wrapper cannot silently leave the - release button on upstream's primary purple. */ + release button on upstream's primary purple. Unlayered, so this beats any + leftover Tailwind fill utilities without a hover twin. */ :root[data-fork="noahhendrickson-t3code"] [data-fork-composer-action="send"][data-fork-composer-send-tone="flat"] { background: #ffffff; @@ -445,12 +446,6 @@ box-shadow: none; } -:root[data-fork="noahhendrickson-t3code"] - [data-fork-composer-action="send"][data-fork-composer-send-tone="flat"]:hover:enabled { - background: #ffffff; - color: #000000; -} - :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-action="stop"] { background: #ea3150; } From 9548cdb314d8691bafd767f287f2b54b5ab836e3 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 27 Jul 2026 20:03:14 -0400 Subject: [PATCH 4/4] fix(fork): dark-scope the flat send fill and restore upstream fallback - .dark on the flat rule: light builds keep upstream's primary fill instead of white-on-white (re-review blocker 1) - revert the className hoist: base + ternary arms match upstream again, so the flat branch keeps bg-primary/90 as the fallback paint if the fork rule ever fails to match (re-review blocker 2) - guard regexes anchor the dark-scoped marker prefix and the attribute assertion is structural, not source-text - CSS comment and manifest intent state the enforced invariant: flat = no stage-channel art, dark mode only Co-Authored-By: Claude Fable 5 --- .fork/customizations.yaml | 13 ++++++++----- .../src/__fork_guards__/forkComposerShell.test.ts | 14 ++++++++------ .../src/components/chat/ComposerPrimaryActions.tsx | 6 +++--- apps/web/src/theme.custom.css | 13 +++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index ed644ff1311..2f3d927f9e0 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -615,10 +615,12 @@ the latch survive thread switches, and "only an empty prompt turns it off" is a rule about one composing session, not the component's lifetime. Which controls sit where is the whole point. Inside the box: the model - pill, the effort pill, and the send/stop button. On a release build the send - button is flat pure white with a black icon + pill, the effort pill, and the send/stop button. In dark mode a send + button without stage-channel art — any stage label that is not Dev/Nightly + — is flat pure white with a black icon (data-fork-composer-send-tone="flat"); Dev and Nightly keep the - stage-channel fill. Everything else is on the + stage-channel fill, and light builds keep upstream's fill. 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 @@ -714,8 +716,9 @@ # 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. Release send also carries - # data-fork-composer-send-tone="flat" for the white/black fill. + # square restyle hangs off it. Send also carries + # data-fork-composer-send-tone ("channel" with stage art, "flat" + # otherwise) for the dark-mode white/black fill. - 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 diff --git a/apps/web/src/__fork_guards__/forkComposerShell.test.ts b/apps/web/src/__fork_guards__/forkComposerShell.test.ts index 3b0661df1e4..5a73ce20bda 100644 --- a/apps/web/src/__fork_guards__/forkComposerShell.test.ts +++ b/apps/web/src/__fork_guards__/forkComposerShell.test.ts @@ -184,16 +184,18 @@ describe("fork guard: fork-composer-shell", () => { it("squares the send and stop buttons, flattens release send, and reddens stop", () => { expect(theme).toMatch(/\[data-fork-composer-action\]\s*\{[\s\S]{0,200}border-radius:\s*6px/u); - // Release (tone=flat) is pure white / black icon — not upstream primary. - // Dev/Nightly keep tone=channel and the stage art; do not collapse them. - expect(primaryActions).toContain( - 'data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"}', + // Flat (no stage art) is pure white / black icon in dark mode only; light + // and unmarked builds keep upstream's fill, so the rule must stay anchored + // on the dark-scoped marker prefix. Dev/Nightly keep tone=channel and the + // stage art; do not collapse them. + expect(primaryActions).toMatch( + /data-fork-composer-send-tone=\{\s*stageBackdropVariant\s*\?\s*"channel"\s*:\s*"flat"\s*\}/u, ); expect(theme).toMatch( - /\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}background:\s*#ffffff/u, + /:root\[data-fork="noahhendrickson-t3code"\]\.dark\s+\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}background:\s*#ffffff/u, ); expect(theme).toMatch( - /\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}color:\s*#000000/u, + /:root\[data-fork="noahhendrickson-t3code"\]\.dark\s+\[data-fork-composer-action="send"\]\[data-fork-composer-send-tone="flat"\][\s\S]{0,200}color:\s*#000000/u, ); expect(theme).toMatch( /\[data-fork-composer-action="stop"\][\s\S]{0,120}background:\s*#ea3150/u, diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index 3c992a12515..c08098ba0a2 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -206,10 +206,10 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ data-fork-composer-send-tone={stageBackdropVariant ? "channel" : "flat"} /* fork:end fork-composer-shell */ className={cn( - "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full transition-all duration-150 enabled:cursor-pointer hover:scale-105 disabled:pointer-events-none disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8", + "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full text-primary-foreground shadow-xs transition-all duration-150 enabled:cursor-pointer enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] hover:scale-105 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:pointer-events-none disabled:opacity-30 disabled:shadow-none disabled:hover:scale-100 sm:h-8 sm:w-8", stageBackdropVariant - ? "bg-transparent text-primary-foreground shadow-xs enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] enabled:shadow-black/24 enabled:hover:brightness-110 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:shadow-none" - : "shadow-none", + ? "bg-transparent enabled:shadow-black/24 enabled:hover:brightness-110" + : "bg-primary/90 enabled:shadow-primary/24 hover:bg-primary", )} {...pointerFocusProps} disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent} diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css index dad9e7c02ff..17e266087ff 100644 --- a/apps/web/src/theme.custom.css +++ b/apps/web/src/theme.custom.css @@ -434,12 +434,13 @@ border-radius: 6px; } -/* Release send: flat pure white with a black icon. Dev/Nightly keep the - stage-channel fill (tone="channel"); the attribute is set from - ComposerPrimaryActions so a missing art wrapper cannot silently leave the - release button on upstream's primary purple. Unlayered, so this beats any - leftover Tailwind fill utilities without a hover twin. */ -:root[data-fork="noahhendrickson-t3code"] +/* Flat send (tone="flat"): in dark mode, a send button without stage-channel + art — any stage label that is not Dev/Nightly — is flat pure white with a + black icon. Dev/Nightly keep the stage-channel fill (tone="channel"). + Unlayered, so this beats upstream's Tailwind fill; the fenced attribute + survives upstream className rewrites. Unmarked or light builds never match + and fall through to upstream's primary fill. */ +:root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-action="send"][data-fork-composer-send-tone="flat"] { background: #ffffff; color: #000000;