authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 21:33:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 21:33:03-07:00
log0e512db34d787b21b827f07c6a62fba7fb00369e
tree042470d52a00edf8e081bf8640e4908c65c6a91c
parent71c9b693137c070b4887b365c68659f1a2fb7aed
signature Signed by PGP key B5690EEEBB952194

refactor(web): composer controls own their look instead of restyling ui components (#13209)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

15 files changed, 100 insertions(+), 137 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+24-29
...@@ -1100,14 +1100,8 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -1100,14 +1100,8 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
1100 render={1100 render={
1101 <ComposerControl1101 <ComposerControl
1102 size={size}1102 size={size}
1103 className={cn(1103 className="shrink-0 whitespace-nowrap"
1104 "shrink-0 whitespace-nowrap",1104 aria-pressed={props.interactionMode === "plan"}
1105 props.interactionMode === "plan"
1106 ? "bg-accent text-accent-foreground hover:bg-accent/80"
1107 : size === "xs"
1108 ? undefined
1109 : "text-secondary-label hover:text-foreground",
1110 )}
1111 type="button"1105 type="button"
1112 onClick={props.onToggleInteractionMode}1106 onClick={props.onToggleInteractionMode}
1113 aria-label={interactionModeTooltip}1107 aria-label={interactionModeTooltip}
...@@ -1152,7 +1146,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -1152,7 +1146,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
1152 <ComposerSelectControl1146 <ComposerSelectControl
1153 data-composer-shortcut="composer.mode"1147 data-composer-shortcut="composer.mode"
1154 size={size}1148 size={size}
1155 className={size === "xs" ? undefined : "font-medium"}
1156 aria-label="Runtime mode"1149 aria-label="Runtime mode"
1157 />1150 />
1158 }1151 }
...@@ -4994,10 +4987,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -4994,10 +4987,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
4994 .slice(restingBlockDefs.length - restingHiddenBlockCount)4987 .slice(restingBlockDefs.length - restingHiddenBlockCount)
4995 .map((def) => def.id);4988 .map((def) => def.id);
4996 const composerControls = showProviderUnavailable ? (4989 const composerControls = showProviderUnavailable ? (
4997 <Button4990 <ComposerControl
4998 type="button"4991 type="button"
4999 size="sm"
5000 variant="ghost"
5001 disabled={!providerSetupInstanceId}4992 disabled={!providerSetupInstanceId}
5002 onClick={() => {4993 onClick={() => {
5003 if (providerSetupInstanceId) {4994 if (providerSetupInstanceId) {
...@@ -5005,11 +4996,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5005,11 +4996,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5005 }4996 }
5006 }}4997 }}
5007 data-chat-provider-unavailable="true"4998 data-chat-provider-unavailable="true"
5008 className="shrink-0 gap-2 px-2 text-secondary-label sm:px-3"4999 className="shrink-0"
5009 >5000 >
5010 <CircleAlertIcon className="size-4" />5001 <CircleAlertIcon className="size-4" />
5011 {providerSetupInstanceId ? "Open provider settings" : "No provider available"}5002 {providerSetupInstanceId ? "Open provider settings" : "No provider available"}
5012 </Button>5003 </ComposerControl>
5013 ) : (5004 ) : (
5014 <>5005 <>
5015 {composerControlsInStrip && restingControlsHaveLeadingContext ? (5006 {composerControlsInStrip && restingControlsHaveLeadingContext ? (
...@@ -6590,9 +6581,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -6590,9 +6581,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
6590 <TooltipTrigger6581 <TooltipTrigger
6591 render={6582 render={
6592 <Button6583 <Button
6593 variant="ghost"6584 variant="overlay"
6594 size="icon-xs"6585 size="icon-xs"
6595 className="absolute bottom-1 left-1 bg-background/85 hover:bg-background/95"6586 className="absolute bottom-1 left-1"
6596 onClick={() =>6587 onClick={() =>
6597 retryAttachmentUpload({6588 retryAttachmentUpload({
6598 environmentId,6589 environmentId,
...@@ -6609,19 +6600,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -6609,19 +6600,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
6609 <TooltipPopup side="top">{upload.reason}</TooltipPopup>6600 <TooltipPopup side="top">{upload.reason}</TooltipPopup>
6610 </Tooltip>6601 </Tooltip>
6611 )}6602 )}
6612 <Button6603 {/* Snap-shot frames reveal their remove button on hover or focus. */}
6613 variant="ghost"6604 <span
6614 size="icon-xs"
6615 className={cn(6605 className={cn(
6616 "absolute right-1 top-1 bg-background/80 hover:bg-background/90",6606 "absolute right-1 top-1 flex",
6617 image.source?.kind === "snap-shot" &&6607 image.source?.kind === "snap-shot" &&
6618 "opacity-0 transition-opacity pointer-coarse:opacity-100 focus-visible:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",6608 "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",
6619 )}6609 )}
6620 onClick={() => removeComposerImage(image.id)}
6621 aria-label={`Remove ${image.name}`}
6622 >6610 >
6623 <XIcon />6611 <Button
6624 </Button>6612 variant="media-close"
6613 size="icon-xs"
6614 onClick={() => removeComposerImage(image.id)}
6615 aria-label={`Remove ${image.name}`}
6616 >
6617 <XIcon />
6618 </Button>
6619 </span>
6625 </SnapShotAttachmentFrame>6620 </SnapShotAttachmentFrame>
6626 );6621 );
6627 })6622 })
...@@ -6685,9 +6680,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -6685,9 +6680,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
6685 <TooltipTrigger6680 <TooltipTrigger
6686 render={6681 render={
6687 <Button6682 <Button
6688 variant="ghost"6683 variant="overlay"
6689 size="icon-xs"6684 size="icon-xs"
6690 className="absolute bottom-1 left-1 bg-background/85 hover:bg-background/95"6685 className="absolute bottom-1 left-1"
6691 onClick={() =>6686 onClick={() =>
6692 retryAttachmentUpload({6687 retryAttachmentUpload({
6693 environmentId,6688 environmentId,
...@@ -6705,9 +6700,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -6705,9 +6700,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
6705 </Tooltip>6700 </Tooltip>
6706 )}6701 )}
6707 <Button6702 <Button
6708 variant="ghost"6703 variant="media-close"
6709 size="icon-xs"6704 size="icon-xs"
6710 className="absolute right-1 top-1 bg-background/80 hover:bg-background/90"6705 className="absolute right-1 top-1"
6711 onClick={() => removeComposerFileFromDraft(file.id)}6706 onClick={() => removeComposerFileFromDraft(file.id)}
6712 aria-label={`Remove ${file.name}`}6707 aria-label={`Remove ${file.name}`}
6713 >6708 >
apps/web/src/components/chat/CompactComposerControlsMenu.tsx+1-2
...@@ -38,8 +38,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls...@@ -38,8 +38,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
38 render={38 render={
39 <ComposerControl39 <ComposerControl
40 size={size}40 size={size}
41 variant="ghost"41 className="shrink-0"
42 className={size === "xs" ? "shrink-0" : "shrink-0 px-2"}
43 aria-label="More composer controls"42 aria-label="More composer controls"
44 data-composer-shortcut={43 data-composer-shortcut={
45 props.traitsMenuContent ? "composer.mode composer.effort" : "composer.mode"44 props.traitsMenuContent ? "composer.mode composer.effort" : "composer.mode"
apps/web/src/components/chat/ComposerControl.tsx+40-38
...@@ -1,45 +1,49 @@...@@ -1,45 +1,49 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { Select as SelectPrimitive } from "@base-ui/react/select";
3import { useRender } from "@base-ui/react/use-render";
1import type { ComponentProps } from "react";4import type { ComponentProps } from "react";
2import { ChevronDownIcon, type LucideIcon } from "lucide-react";5import { ChevronDownIcon, type LucideIcon } from "lucide-react";
36
4import { cn } from "~/lib/utils";7import { cn } from "~/lib/utils";
5import { Button } from "../ui/button";
6import { SelectTrigger } from "../ui/select";
7import { Separator } from "../ui/separator";8import { Separator } from "../ui/separator";
89
9export type ComposerControlSize = "sm" | "xs";10export type ComposerControlSize = "sm" | "xs";
1011
11type ComposerControlProps = Omit<ComponentProps<typeof Button>, "size"> & {12/**
12 size?: ComposerControlSize;13 * The composer toolbar's control look. `sm` is the expanded toolbar; `xs` is the dimmer resting
13};14 * strip. `aria-pressed` marks a toggle that is on (plan mode). This is an app control, not a
15 * restyled Button, so it owns its classes.
16 */
17function composerControlClassName(size: ComposerControlSize, className?: string) {
18 return cn(
19 "relative inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap rounded-[var(--control-radius)] border border-transparent text-base outline-none hover:bg-accent data-pressed:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-disabled:pointer-events-none data-disabled:opacity-64 pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&:active:not([aria-haspopup])]:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:-mx-0.5 [&_svg[data-composer-control-icon]]:mx-0 [&_svg:not([class*='text-'])]:text-[var(--control-icon-color)]",
20 size === "xs"
21 ? "h-7 gap-1 px-[calc(--spacing(2)-1px)] font-normal text-muted-foreground/70 text-sm [--control-icon-color:currentColor] hover:text-foreground/80 sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5 [&_svg[data-composer-control-chevron]]:ms-0 [&_svg[data-composer-control-chevron]]:-me-1"
22 : "h-7 gap-1.5 px-2.5 font-medium text-secondary-label [--control-icon-color:var(--contrast-muted-foreground)] hover:text-foreground sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4",
23 "aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:hover:bg-accent/80",
24 className,
25 );
26}
1427
15type ComposerSelectControlProps = Omit<ComponentProps<typeof SelectTrigger>, "size"> & {28type ComposerControlProps = useRender.ComponentProps<"button"> & {
16 size?: ComposerControlSize;29 size?: ComposerControlSize;
17};30};
1831
19const composerControlClassName =
20 "rounded-[var(--control-radius)] text-secondary-label transition-none hover:text-foreground [&_svg[data-composer-control-chevron]]:-mx-0.5 [&_svg[data-composer-control-icon]]:mx-0";
21const expandedComposerControlClassName = "h-7 min-h-7 gap-1.5 px-2.5";
22const restingComposerControlClassName =
23 "[--control-icon-color:currentColor] font-normal text-muted-foreground/70 hover:text-foreground/80 [&_svg[data-composer-control-chevron]]:-me-1 [&_svg[data-composer-control-chevron]]:ms-0";
24
25export function ComposerControl({32export function ComposerControl({
26 className,33 className,
27 size = "sm",34 size = "sm",
28 variant = "ghost",35 render,
29 ...props36 ...props
30}: ComposerControlProps) {37}: ComposerControlProps) {
31 return (38 const defaultProps = {
32 <Button39 className: composerControlClassName(size, className),
33 className={cn(40 type: render ? undefined : ("button" as const),
34 composerControlClassName,41 };
35 size === "xs" ? restingComposerControlClassName : expandedComposerControlClassName,42 return useRender({
36 className,43 defaultTagName: "button",
37 )}44 props: mergeProps<"button">(defaultProps, props),
38 size={size}45 render,
39 variant={variant}46 });
40 {...props}
41 />
42 );
43}47}
4448
45export function ComposerControlIcon({49export function ComposerControlIcon({
...@@ -105,21 +109,19 @@ export function ComposerControlSeparator({...@@ -105,21 +109,19 @@ export function ComposerControlSeparator({
105109
106export function ComposerSelectControl({110export function ComposerSelectControl({
107 className,111 className,
112 children,
108 size = "sm",113 size = "sm",
109 variant = "ghost",
110 ...props114 ...props
111}: ComposerSelectControlProps) {115}: Omit<SelectPrimitive.Trigger.Props, "className"> & {
116 className?: string | undefined;
117 size?: ComposerControlSize;
118}) {
112 return (119 return (
113 <SelectTrigger120 <SelectPrimitive.Trigger className={composerControlClassName(size, className)} {...props}>
114 className={cn(121 {children}
115 composerControlClassName,122 <SelectPrimitive.Icon>
116 size === "xs" ? restingComposerControlClassName : expandedComposerControlClassName,123 <ComposerControlChevron size={size} />
117 className,124 </SelectPrimitive.Icon>
118 )}125 </SelectPrimitive.Trigger>
119 icon={<ComposerControlChevron size={size} />}
120 size={size}
121 variant={variant}
122 {...props}
123 />
124 );126 );
125}127}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+20-24
...@@ -52,6 +52,11 @@ const formatPendingPrimaryActionLabel = (input: {...@@ -52,6 +52,11 @@ const formatPendingPrimaryActionLabel = (input: {
52 return input.questionIndex > 0 ? "Submit answers" : "Submit answer";52 return input.questionIndex > 0 ? "Submit answers" : "Submit answer";
53};53};
5454
55// The composer's labeled primary actions (Submit, Refine, Implement) share the send button's
56// message-action pill, so they are composer-owned buttons rather than restyled Buttons.
57const messageActionPillClassName =
58 "inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-full bg-message-action font-medium text-base text-message-action-foreground shadow-xs shadow-message-action/24 outline-none hover:bg-message-action-hover focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 disabled:shadow-none sm:text-sm";
59
55const preventPointerFocus: PointerEventHandler<HTMLElement> = (event) => {60const preventPointerFocus: PointerEventHandler<HTMLElement> = (event) => {
56 event.preventDefault();61 event.preventDefault();
57};62};
...@@ -112,7 +117,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -112,7 +117,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
112 <Button117 <Button
113 size="icon-sm"118 size="icon-sm"
114 variant="outline"119 variant="outline"
115 className="rounded-full"
116 {...pointerFocusProps}120 {...pointerFocusProps}
117 onClick={onPreviousPendingQuestion}121 onClick={onPreviousPendingQuestion}
118 disabled={pendingAction.isResponding}122 disabled={pendingAction.isResponding}
...@@ -124,7 +128,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -124,7 +128,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
124 <Button128 <Button
125 size="sm"129 size="sm"
126 variant="outline"130 variant="outline"
127 className="rounded-full"
128 {...pointerFocusProps}131 {...pointerFocusProps}
129 onClick={onPreviousPendingQuestion}132 onClick={onPreviousPendingQuestion}
130 disabled={pendingAction.isResponding}133 disabled={pendingAction.isResponding}
...@@ -133,13 +136,9 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -133,13 +136,9 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
133 </Button>136 </Button>
134 )137 )
135 ) : null}138 ) : null}
136 <Button139 <button
137 type="submit"140 type="submit"
138 size="sm"141 className={cn(messageActionPillClassName, "h-8 sm:h-7", compact ? "px-3" : "px-4")}
139 className={cn(
140 "rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover",
141 compact ? "px-3" : "px-4",
142 )}
143 {...pointerFocusProps}142 {...pointerFocusProps}
144 disabled={143 disabled={
145 isEnvironmentUnavailable ||144 isEnvironmentUnavailable ||
...@@ -153,7 +152,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -153,7 +152,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
153 isResponding: pendingAction.isResponding,152 isResponding: pendingAction.isResponding,
154 questionIndex: pendingAction.questionIndex,153 questionIndex: pendingAction.questionIndex,
155 })}154 })}
156 </Button>155 </button>
157 </div>156 </div>
158 );157 );
159 }158 }
...@@ -161,39 +160,36 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -161,39 +160,36 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
161 if (showPlanFollowUpPrompt) {160 if (showPlanFollowUpPrompt) {
162 if (promptHasText) {161 if (promptHasText) {
163 return (162 return (
164 <Button163 <button
165 type="submit"164 type="submit"
166 size="sm"165 className={cn(messageActionPillClassName, "h-9 sm:h-8", compact ? "px-3" : "px-4")}
167 className={cn(
168 "rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover",
169 compact ? "h-9 px-3 sm:h-8" : "h-9 px-4 sm:h-8",
170 )}
171 {...pointerFocusProps}166 {...pointerFocusProps}
172 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}167 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
173 >168 >
174 {isConnecting || isSendBusy ? "Sending..." : "Refine"}169 {isConnecting || isSendBusy ? "Sending..." : "Refine"}
175 </Button>170 </button>
176 );171 );
177 }172 }
178173
179 return (174 return (
180 <div data-chat-composer-implement-actions="true" className="flex items-center justify-end">175 <div data-chat-composer-implement-actions="true" className="flex items-center justify-end">
181 <Button176 <button
182 type="submit"177 type="submit"
183 size="sm"178 className={cn(messageActionPillClassName, "h-9 rounded-r-none px-4 sm:h-8")}
184 className="h-9 rounded-l-full rounded-r-none bg-message-action px-4 text-message-action-foreground hover:bg-message-action-hover sm:h-8"
185 {...pointerFocusProps}179 {...pointerFocusProps}
186 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}180 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
187 >181 >
188 {isConnecting || isSendBusy ? "Sending..." : "Implement"}182 {isConnecting || isSendBusy ? "Sending..." : "Implement"}
189 </Button>183 </button>
190 <Menu>184 <Menu>
191 <MenuTrigger185 <MenuTrigger
192 render={186 render={
193 <Button187 <button
194 size="sm"188 type="button"
195 variant="default"189 className={cn(
196 className="h-9 rounded-l-none rounded-r-full border-l-message-action-foreground/20 bg-message-action px-2 text-message-action-foreground hover:bg-message-action-hover sm:h-8"190 messageActionPillClassName,
191 "h-9 rounded-l-none border-l border-message-action-foreground/20 px-2 sm:h-8",
192 )}
197 aria-label="Implementation actions"193 aria-label="Implementation actions"
198 {...pointerFocusProps}194 {...pointerFocusProps}
199 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}195 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
apps/web/src/components/chat/ModelListRow.tsx+4-8
...@@ -53,10 +53,9 @@ export const ModelListRow = memo(function ModelListRow(props: {...@@ -53,10 +53,9 @@ export const ModelListRow = memo(function ModelListRow(props: {
53 value={modelPickerModelKey(props.instanceId, props.model.slug)}53 value={modelPickerModelKey(props.instanceId, props.model.slug)}
54 disabled={Boolean(props.disabledReason)}54 disabled={Boolean(props.disabledReason)}
55 className={cn(55 className={cn(
56 "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]",56 "group relative w-full !min-w-0 max-w-full cursor-pointer",
57 "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))]",
58 props.disabledReason &&57 props.disabledReason &&
59 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",58 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed",
60 )}59 )}
61 >60 >
62 <div className="min-w-0 flex-1 text-left">61 <div className="min-w-0 flex-1 text-left">
...@@ -103,11 +102,8 @@ export const ModelListRow = memo(function ModelListRow(props: {...@@ -103,11 +102,8 @@ export const ModelListRow = memo(function ModelListRow(props: {
103 render={102 render={
104 <Button103 <Button
105 size="icon-xs"104 size="icon-xs"
106 variant="ghost"105 variant="ghost-muted"
107 className={cn(106 className="-mr-1 shrink-0"
108 "-mr-1 shrink-0 text-muted-foreground/70 opacity-64 transition-[color,opacity] hover:text-foreground hover:opacity-100 group-hover:opacity-100",
109 props.isFavorite && "text-foreground opacity-100",
110 )}
111 onClick={(event) => {107 onClick={(event) => {
112 event.stopPropagation();108 event.stopPropagation();
113 props.onToggleFavorite();109 props.onToggleFavorite();
apps/web/src/components/chat/ModelPickerContent.tsx+7-11
...@@ -39,7 +39,7 @@ import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"...@@ -39,7 +39,7 @@ import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"
39import { cn } from "~/lib/utils";39import { cn } from "~/lib/utils";
40import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";40import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";
41import { TooltipProvider } from "../ui/tooltip";41import { TooltipProvider } from "../ui/tooltip";
42import { Button } from "../ui/button";42import { InlineButton } from "../ui/button";
43import {43import {
44 isProviderInstancePickerReady,44 isProviderInstancePickerReady,
45 isProviderInstancePickerVisible,45 isProviderInstancePickerVisible,
...@@ -941,7 +941,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -941,7 +941,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
941941
942 {/* Model list */}942 {/* Model list */}
943 <div className="relative min-h-0 flex-1 overflow-hidden pr-px">943 <div className="relative min-h-0 flex-1 overflow-hidden pr-px">
944 <ComboboxListVirtualized className="not-empty:p-0">944 <ComboboxListVirtualized>
945 <LegendList<string>945 <LegendList<string>
946 ref={modelListRef}946 ref={modelListRef}
947 data={filteredItemKeys}947 data={filteredItemKeys}
...@@ -955,7 +955,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -955,7 +955,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
955 index={index}955 index={index}
956 value={modelKey}956 value={modelKey}
957 aria-expanded={legacySection.isExpanded}957 aria-expanded={legacySection.isExpanded}
958 className="group w-full cursor-pointer rounded-md px-2 py-2"958 className="group w-full cursor-pointer"
959 >959 >
960 <div className="min-w-0 flex-1 text-left">960 <div className="min-w-0 flex-1 text-left">
961 <div className="text-xs font-medium leading-snug">Legacy models</div>961 <div className="text-xs font-medium leading-snug">Legacy models</div>
...@@ -1031,26 +1031,22 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -1031,26 +1031,22 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
1031 <p className="line-clamp-3 text-muted-foreground">1031 <p className="line-clamp-3 text-muted-foreground">
1032 {getProviderStatusMessage(entry.snapshot)}1032 {getProviderStatusMessage(entry.snapshot)}
1033 </p>1033 </p>
1034 <Button1034 <InlineButton
1035 className="mt-1 px-0 text-foreground"1035 className="mt-1"
1036 onClick={() => {1036 onClick={() => {
1037 props.onRequestClose?.();1037 props.onRequestClose?.();
1038 props.onOpenProviderSetup?.(entry.instanceId);1038 props.onOpenProviderSetup?.(entry.instanceId);
1039 }}1039 }}
1040 size="xs"
1041 variant="link"
1042 >1040 >
1043 {providerSetupEntries.length > 11041 {providerSetupEntries.length > 1
1044 ? `Set up ${entry.displayName}`1042 ? `Set up ${entry.displayName}`
1045 : "Open provider setup"}1043 : "Open provider setup"}
1046 </Button>1044 </InlineButton>
1047 </div>1045 </div>
1048 ))}1046 ))}
1049 </div>1047 </div>
1050 ) : (1048 ) : (
1051 <ComboboxEmpty className="not-empty:py-6 empty:h-0 text-xs font-normal leading-snug">1049 <ComboboxEmpty className="empty:h-0">No models found</ComboboxEmpty>
1052 No models found
1053 </ComboboxEmpty>
1054 )}1050 )}
1055 </div>1051 </div>
1056 </Combobox>1052 </Combobox>
apps/web/src/components/chat/ProviderModelPicker.tsx-3
...@@ -6,7 +6,6 @@ import {...@@ -6,7 +6,6 @@ import {
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { memo, useEffect, useMemo, useState } from "react";7import { memo, useEffect, useMemo, useState } from "react";
8import { Badge } from "../ui/badge";8import { Badge } from "../ui/badge";
9import type { ButtonVariant } from "../ui/button";
10import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";9import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
11import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";10import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
12import { cn } from "~/lib/utils";11import { cn } from "~/lib/utils";
...@@ -48,7 +47,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -48,7 +47,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
48 disabled?: boolean;47 disabled?: boolean;
49 terminalOpen?: boolean;48 terminalOpen?: boolean;
50 open?: boolean;49 open?: boolean;
51 triggerVariant?: ButtonVariant;
52 triggerClassName?: string;50 triggerClassName?: string;
53 /** Aggregate settings can show a neutral value without claiming one provider is selected. */51 /** Aggregate settings can show a neutral value without claiming one provider is selected. */
54 triggerLabel?: string;52 triggerLabel?: string;
...@@ -206,7 +204,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -206,7 +204,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
206 render={204 render={
207 <ComposerControl205 <ComposerControl
208 aria-label={props.triggerAriaLabel ?? allModelNames}206 aria-label={props.triggerAriaLabel ?? allModelNames}
209 variant={props.triggerVariant ?? "ghost"}
210 size={size}207 size={size}
211 data-chat-provider-model-picker="true"208 data-chat-provider-model-picker="true"
212 className={cn(209 className={cn(
apps/web/src/components/chat/TraitsPicker.tsx-4
...@@ -17,7 +17,6 @@ import {...@@ -17,7 +17,6 @@ import {
17} from "@t3tools/shared/model";17} from "@t3tools/shared/model";
18import { memo, useCallback } from "react";18import { memo, useCallback } from "react";
19import { GaugeIcon, ZapIcon } from "lucide-react";19import { GaugeIcon, ZapIcon } from "lucide-react";
20import type { ButtonVariant } from "../ui/button";
21import {20import {
22 Menu,21 Menu,
23 MenuGroup,22 MenuGroup,
...@@ -276,7 +275,6 @@ export interface TraitsMenuContentProps {...@@ -276,7 +275,6 @@ export interface TraitsMenuContentProps {
276 modelOptions?: ProviderOptions | null | undefined;275 modelOptions?: ProviderOptions | null | undefined;
277 allowPromptInjectedEffort?: boolean;276 allowPromptInjectedEffort?: boolean;
278 planModeEnabled: boolean;277 planModeEnabled: boolean;
279 triggerVariant?: ButtonVariant;
280 triggerClassName?: string;278 triggerClassName?: string;
281 isComposerOwned?: boolean;279 isComposerOwned?: boolean;
282}280}
...@@ -543,7 +541,6 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -543,7 +541,6 @@ export const TraitsPicker = memo(function TraitsPicker({
543 modelOptions,541 modelOptions,
544 allowPromptInjectedEffort = true,542 allowPromptInjectedEffort = true,
545 planModeEnabled,543 planModeEnabled,
546 triggerVariant,
547 triggerClassName,544 triggerClassName,
548 isComposerOwned,545 isComposerOwned,
549 size = "sm",546 size = "sm",
...@@ -622,7 +619,6 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -622,7 +619,6 @@ export const TraitsPicker = memo(function TraitsPicker({
622 <ComposerControl619 <ComposerControl
623 aria-label={accessibleLabel}620 aria-label={accessibleLabel}
624 data-composer-shortcut={isComposerOwned ? "composer.effort" : undefined}621 data-composer-shortcut={isComposerOwned ? "composer.effort" : undefined}
625 variant={triggerVariant ?? "ghost"}
626 size={size}622 size={size}
627 className={cn(623 className={cn(
628 isCodexStyle624 isCodexStyle
apps/web/src/components/chat/composerProviderState.tsx-4
...@@ -15,7 +15,6 @@ import {...@@ -15,7 +15,6 @@ import {
15} from "@t3tools/shared/model";15} from "@t3tools/shared/model";
16import type { ReactNode } from "react";16import type { ReactNode } from "react";
1717
18import type { ButtonVariant } from "../ui/button";
19import type { DraftId } from "../../composerDraftStore";18import type { DraftId } from "../../composerDraftStore";
20import { getProviderModelCapabilities } from "../../providerModels";19import { getProviderModelCapabilities } from "../../providerModels";
21import type { ComposerControlSize } from "./ComposerControl";20import type { ComposerControlSize } from "./ComposerControl";
...@@ -54,7 +53,6 @@ type TraitsRenderInput = {...@@ -54,7 +53,6 @@ type TraitsRenderInput = {
54 planModeEnabled: boolean;53 planModeEnabled: boolean;
55 size?: ComposerControlSize;54 size?: ComposerControlSize;
56 hidden?: boolean;55 hidden?: boolean;
57 triggerVariant?: ButtonVariant;
58 triggerClassName?: string;56 triggerClassName?: string;
59 isComposerOwned?: boolean;57 isComposerOwned?: boolean;
60};58};
...@@ -177,7 +175,6 @@ function renderTraitsControl(...@@ -177,7 +175,6 @@ function renderTraitsControl(
177 planModeEnabled,175 planModeEnabled,
178 size,176 size,
179 hidden,177 hidden,
180 triggerVariant,
181 triggerClassName,178 triggerClassName,
182 isComposerOwned,179 isComposerOwned,
183 } = input;180 } = input;
...@@ -216,7 +213,6 @@ function renderTraitsControl(...@@ -216,7 +213,6 @@ function renderTraitsControl(
216 planModeEnabled={planModeEnabled}213 planModeEnabled={planModeEnabled}
217 {...(size !== undefined ? { size } : {})}214 {...(size !== undefined ? { size } : {})}
218 {...(hidden !== undefined ? { hidden } : {})}215 {...(hidden !== undefined ? { hidden } : {})}
219 {...(triggerVariant !== undefined ? { triggerVariant } : {})}
220 {...(triggerClassName !== undefined ? { triggerClassName } : {})}216 {...(triggerClassName !== undefined ? { triggerClassName } : {})}
221 {...(isComposerOwned ? { isComposerOwned } : {})}217 {...(isComposerOwned ? { isComposerOwned } : {})}
222 />218 />
apps/web/src/components/settings/ProjectDefaultsSettings.tsx-2
...@@ -162,7 +162,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting...@@ -162,7 +162,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
162 lockedProvider={null}162 lockedProvider={null}
163 instanceEntries={entries}163 instanceEntries={entries}
164 modelOptionsByInstance={modelOptions}164 modelOptionsByInstance={modelOptions}
165 triggerVariant="outline"
166 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}165 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
167 {...(mixedModel ? { triggerLabel: "Mixed" } : {})}166 {...(mixedModel ? { triggerLabel: "Mixed" } : {})}
168 getModelDisabledReason={modelDisabledReason}167 getModelDisabledReason={modelDisabledReason}
...@@ -187,7 +186,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting...@@ -187,7 +186,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
187 modelOptions={selection.options ?? []}186 modelOptions={selection.options ?? []}
188 allowPromptInjectedEffort={false}187 allowPromptInjectedEffort={false}
189 planModeEnabled={settings.planModeEnabled}188 planModeEnabled={settings.planModeEnabled}
190 triggerVariant="outline"
191 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}189 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
192 onModelOptionsChange={(options) =>190 onModelOptionsChange={(options) =>
193 setModel(createModelSelection(selection.instanceId, selection.model, options))191 setModel(createModelSelection(selection.instanceId, selection.model, options))
apps/web/src/components/settings/SettingsPanels.tsx-2
...@@ -3113,7 +3113,6 @@ export function GeneralSettingsPanel() {...@@ -3113,7 +3113,6 @@ export function GeneralSettingsPanel() {
3113 lockedProvider={null}3113 lockedProvider={null}
3114 instanceEntries={textGenerationModelInstanceEntries}3114 instanceEntries={textGenerationModelInstanceEntries}
3115 modelOptionsByInstance={textGenerationModelOptionsByInstance}3115 modelOptionsByInstance={textGenerationModelOptionsByInstance}
3116 triggerVariant="outline"
3117 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}3116 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
3118 {...(mixedTextGenerationModel ? { triggerLabel: "Mixed" } : {})}3117 {...(mixedTextGenerationModel ? { triggerLabel: "Mixed" } : {})}
3119 getModelDisabledReason={textGenerationModelDisabledReason}3118 getModelDisabledReason={textGenerationModelDisabledReason}
...@@ -3164,7 +3163,6 @@ export function GeneralSettingsPanel() {...@@ -3164,7 +3163,6 @@ export function GeneralSettingsPanel() {
3164 modelOptions={textGenModelOptions}3163 modelOptions={textGenModelOptions}
3165 allowPromptInjectedEffort={false}3164 allowPromptInjectedEffort={false}
3166 planModeEnabled={settings.planModeEnabled}3165 planModeEnabled={settings.planModeEnabled}
3167 triggerVariant="outline"
3168 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}3166 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
3169 onModelOptionsChange={(nextOptions) => {3167 onModelOptionsChange={(nextOptions) => {
3170 updateSettings({3168 updateSettings({
apps/web/src/components/settings/SourceControlWritingSettings.tsx-1
...@@ -304,7 +304,6 @@ export function SourceControlWritingSettingsSection() {...@@ -304,7 +304,6 @@ export function SourceControlWritingSettingsSection() {
304 lockedProvider={null}304 lockedProvider={null}
305 instanceEntries={instanceEntries}305 instanceEntries={instanceEntries}
306 modelOptionsByInstance={modelOptionsByInstance}306 modelOptionsByInstance={modelOptionsByInstance}
307 triggerVariant="outline"
308 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}307 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
309 triggerAriaLabel="Source control writer model"308 triggerAriaLabel="Source control writer model"
310 {...(mixedWriterModel ? { triggerLabel: "Mixed" } : {})}309 {...(mixedWriterModel ? { triggerLabel: "Mixed" } : {})}
apps/web/src/components/settings/settingsLayout.tsx+2-7
...@@ -137,13 +137,8 @@ export function SettingsSearchTarget({...@@ -137,13 +137,8 @@ export function SettingsSearchTarget({
137 );137 );
138}138}
139139
140/**140/** Layout for the composer model/traits pickers in a settings row: drop the composer's max-width. */
141 * Trigger classes for the composer model/traits pickers when they sit in a141export const SETTINGS_PICKER_TRIGGER_CLASSNAME = "min-w-0 max-w-none shrink-0";
142 * settings row: match the `sm` control box (the composer pins them to 28px at
143 * every breakpoint) and drop the composer's max-width.
144 */
145export const SETTINGS_PICKER_TRIGGER_CLASSNAME =
146 "h-8 min-h-8 min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground sm:h-7 sm:min-h-7";
147142
148/** Info affordance explaining how a setting interacts with the shared background policy. */143/** Info affordance explaining how a setting interacts with the shared background policy. */
149export function PolicyTooltip({ children }: { readonly children: string }) {144export function PolicyTooltip({ children }: { readonly children: string }) {
apps/web/src/components/ui/combobox.tsx+1-1
...@@ -194,7 +194,7 @@ function ComboboxItem({...@@ -194,7 +194,7 @@ function ComboboxItem({
194 return (194 return (
195 <ComboboxPrimitive.Item195 <ComboboxPrimitive.Item
196 className={cn(196 className={cn(
197 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none hover:bg-accent data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",197 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none not-data-disabled:hover:bg-accent data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
198 className,198 className,
199 )}199 )}
200 data-slot="combobox-item"200 data-slot="combobox-item"
scripts/lint-restyle-ceiling.ts+1-1
...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
10// migrate a file, and delete this script when the rule becomes an error.10// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 101;11export const RESTYLE_CEILING = 0;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414