diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 7955e1e02b25..b377439882d5 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1100,14 +1100,8 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop render={ } @@ -4994,10 +4987,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) .slice(restingBlockDefs.length - restingHiddenBlockCount) .map((def) => def.id); const composerControls = showProviderUnavailable ? ( - { if (providerSetupInstanceId) { @@ -5005,11 +4996,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } }} data-chat-provider-unavailable="true" - className="shrink-0 gap-2 px-2 text-secondary-label sm:px-3" + className="shrink-0" > {providerSetupInstanceId ? "Open provider settings" : "No provider available"} - + ) : ( <> {composerControlsInStrip && restingControlsHaveLeadingContext ? ( @@ -6590,9 +6581,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) retryAttachmentUpload({ environmentId, @@ -6609,19 +6600,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {upload.reason} )} - removeComposerImage(image.id)} - aria-label={`Remove ${image.name}`} > - - + removeComposerImage(image.id)} + aria-label={`Remove ${image.name}`} + > + + + ); }) @@ -6685,9 +6680,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) retryAttachmentUpload({ environmentId, @@ -6705,9 +6700,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) )} removeComposerFileFromDraft(file.id)} aria-label={`Remove ${file.name}`} > diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index 1969e2e9de36..80be669a84a8 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -38,8 +38,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls render={ , "size"> & { - size?: ComposerControlSize; -}; +/** + * The composer toolbar's control look. `sm` is the expanded toolbar; `xs` is the dimmer resting + * strip. `aria-pressed` marks a toggle that is on (plan mode). This is an app control, not a + * restyled Button, so it owns its classes. + */ +function composerControlClassName(size: ComposerControlSize, className?: string) { + return cn( + "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)]", + size === "xs" + ? "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" + : "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", + "aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:hover:bg-accent/80", + className, + ); +} -type ComposerSelectControlProps = Omit, "size"> & { +type ComposerControlProps = useRender.ComponentProps<"button"> & { size?: ComposerControlSize; }; -const composerControlClassName = - "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"; -const expandedComposerControlClassName = "h-7 min-h-7 gap-1.5 px-2.5"; -const restingComposerControlClassName = - "[--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"; - export function ComposerControl({ className, size = "sm", - variant = "ghost", + render, ...props }: ComposerControlProps) { - return ( - - ); + const defaultProps = { + className: composerControlClassName(size, className), + type: render ? undefined : ("button" as const), + }; + return useRender({ + defaultTagName: "button", + props: mergeProps<"button">(defaultProps, props), + render, + }); } export function ComposerControlIcon({ @@ -105,21 +109,19 @@ export function ComposerControlSeparator({ export function ComposerSelectControl({ className, + children, size = "sm", - variant = "ghost", ...props -}: ComposerSelectControlProps) { +}: Omit & { + className?: string | undefined; + size?: ComposerControlSize; +}) { return ( - } - size={size} - variant={variant} - {...props} - /> + + {children} + + + + ); } diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index f0ebf05c6386..4cdce3c96ad7 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -52,6 +52,11 @@ const formatPendingPrimaryActionLabel = (input: { return input.questionIndex > 0 ? "Submit answers" : "Submit answer"; }; +// The composer's labeled primary actions (Submit, Refine, Implement) share the send button's +// message-action pill, so they are composer-owned buttons rather than restyled Buttons. +const messageActionPillClassName = + "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"; + const preventPointerFocus: PointerEventHandler = (event) => { event.preventDefault(); }; @@ -112,7 +117,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ ) ) : null} - + ); } @@ -161,39 +160,36 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ if (showPlanFollowUpPrompt) { if (promptHasText) { return ( - {isConnecting || isSendBusy ? "Sending..." : "Refine"} - + ); } return ( - {isConnecting || isSendBusy ? "Sending..." : "Implement"} - + @@ -103,11 +102,8 @@ export const ModelListRow = memo(function ModelListRow(props: { render={ { event.stopPropagation(); props.onToggleFavorite(); diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index eb526d7e1e67..57a04b1a2f24 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -39,7 +39,7 @@ import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings" import { cn } from "~/lib/utils"; import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; import { TooltipProvider } from "../ui/tooltip"; -import { Button } from "../ui/button"; +import { InlineButton } from "../ui/button"; import { isProviderInstancePickerReady, isProviderInstancePickerVisible, @@ -941,7 +941,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { {/* Model list */} - + ref={modelListRef} data={filteredItemKeys} @@ -955,7 +955,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { index={index} value={modelKey} aria-expanded={legacySection.isExpanded} - className="group w-full cursor-pointer rounded-md px-2 py-2" + className="group w-full cursor-pointer" > Legacy models @@ -1031,26 +1031,22 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { {getProviderStatusMessage(entry.snapshot)} - { props.onRequestClose?.(); props.onOpenProviderSetup?.(entry.instanceId); }} - size="xs" - variant="link" > {providerSetupEntries.length > 1 ? `Set up ${entry.displayName}` : "Open provider setup"} - + ))} ) : ( - - No models found - + No models found )} diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 21c90b02131c..4211f994b197 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -6,7 +6,6 @@ import { } from "@t3tools/contracts"; import { memo, useEffect, useMemo, useState } from "react"; import { Badge } from "../ui/badge"; -import type { ButtonVariant } from "../ui/button"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; @@ -48,7 +47,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { disabled?: boolean; terminalOpen?: boolean; open?: boolean; - triggerVariant?: ButtonVariant; triggerClassName?: string; /** Aggregate settings can show a neutral value without claiming one provider is selected. */ triggerLabel?: string; @@ -206,7 +204,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { render={ diff --git a/apps/web/src/components/settings/ProjectDefaultsSettings.tsx b/apps/web/src/components/settings/ProjectDefaultsSettings.tsx index c39b8cb3c5b4..62a58307aaf5 100644 --- a/apps/web/src/components/settings/ProjectDefaultsSettings.tsx +++ b/apps/web/src/components/settings/ProjectDefaultsSettings.tsx @@ -162,7 +162,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting lockedProvider={null} instanceEntries={entries} modelOptionsByInstance={modelOptions} - triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} {...(mixedModel ? { triggerLabel: "Mixed" } : {})} getModelDisabledReason={modelDisabledReason} @@ -187,7 +186,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting modelOptions={selection.options ?? []} allowPromptInjectedEffort={false} planModeEnabled={settings.planModeEnabled} - triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} onModelOptionsChange={(options) => setModel(createModelSelection(selection.instanceId, selection.model, options)) diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 6aeeb269f5a0..bb3ba34cbcd4 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -3113,7 +3113,6 @@ export function GeneralSettingsPanel() { lockedProvider={null} instanceEntries={textGenerationModelInstanceEntries} modelOptionsByInstance={textGenerationModelOptionsByInstance} - triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} {...(mixedTextGenerationModel ? { triggerLabel: "Mixed" } : {})} getModelDisabledReason={textGenerationModelDisabledReason} @@ -3164,7 +3163,6 @@ export function GeneralSettingsPanel() { modelOptions={textGenModelOptions} allowPromptInjectedEffort={false} planModeEnabled={settings.planModeEnabled} - triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} onModelOptionsChange={(nextOptions) => { updateSettings({ diff --git a/apps/web/src/components/settings/SourceControlWritingSettings.tsx b/apps/web/src/components/settings/SourceControlWritingSettings.tsx index a4d4e1f950c3..362e6032267d 100644 --- a/apps/web/src/components/settings/SourceControlWritingSettings.tsx +++ b/apps/web/src/components/settings/SourceControlWritingSettings.tsx @@ -304,7 +304,6 @@ export function SourceControlWritingSettingsSection() { lockedProvider={null} instanceEntries={instanceEntries} modelOptionsByInstance={modelOptionsByInstance} - triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} triggerAriaLabel="Source control writer model" {...(mixedWriterModel ? { triggerLabel: "Mixed" } : {})} diff --git a/apps/web/src/components/settings/settingsLayout.tsx b/apps/web/src/components/settings/settingsLayout.tsx index 7f2a56f33122..cc0ecf807b85 100644 --- a/apps/web/src/components/settings/settingsLayout.tsx +++ b/apps/web/src/components/settings/settingsLayout.tsx @@ -137,13 +137,8 @@ export function SettingsSearchTarget({ ); } -/** - * Trigger classes for the composer model/traits pickers when they sit in a - * settings row: match the `sm` control box (the composer pins them to 28px at - * every breakpoint) and drop the composer's max-width. - */ -export const SETTINGS_PICKER_TRIGGER_CLASSNAME = - "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"; +/** Layout for the composer model/traits pickers in a settings row: drop the composer's max-width. */ +export const SETTINGS_PICKER_TRIGGER_CLASSNAME = "min-w-0 max-w-none shrink-0"; /** Info affordance explaining how a setting interacts with the shared background policy. */ export function PolicyTooltip({ children }: { readonly children: string }) { diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 0704746e42ef..4020df1653cc 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -194,7 +194,7 @@ function ComboboxItem({ return (
{getProviderStatusMessage(entry.snapshot)}