diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx
index 2c9312528..7af64abab 100644
--- a/packages/app/src/components/combined-model-selector.tsx
+++ b/packages/app/src/components/combined-model-selector.tsx
@@ -12,14 +12,8 @@ import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb as platformIsWeb } from "@/constants/platform";
-import {
- AlertTriangle,
- ChevronDown,
- ChevronRight,
- Search,
- Settings,
- Star,
-} from "lucide-react-native";
+import { AlertTriangle, ChevronRight, Search, Settings, Star } from "lucide-react-native";
+import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { SheetHeader } from "@/components/adaptive-modal-sheet";
import { useProviderSettingsStore } from "@/stores/provider-settings-store";
@@ -784,35 +778,43 @@ export function CombinedModelSelector({
return (
<>
-
- {renderTrigger ? (
- renderTrigger({
+ {renderTrigger ? (
+
+ {renderTrigger({
selectedModelLabel: triggerLabel,
onPress: handleTriggerPress,
disabled,
isOpen,
- })
- ) : (
- <>
- {ProviderIcon ? (
-
- ) : null}
-
- {triggerLabel}
-
-
- >
- )}
-
+ })}
+
+ ) : (
+
+ {ProviderIcon ? (
+
+ ) : null}
+
+ {triggerLabel}
+
+
+ )}
({
+ color: theme.colors.foregroundMuted,
+});
+
+interface TriggerState {
+ pressed: boolean;
+ hovered: boolean;
+}
+
+type TriggerStyleProp = StyleProp | ((state: TriggerState) => StyleProp);
+
+interface ComboboxTriggerProps extends Omit {
+ style?: TriggerStyleProp;
+ children?: ReactNode;
+ chevron?: ReactNode | null;
+}
+
+export const ComboboxTrigger = forwardRef(function ComboboxTrigger(
+ { children, chevron, style, ...props },
+ ref,
+): ReactElement {
+ const pressableStyle = useCallback(
+ ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
+ if (typeof style === "function") {
+ return style({ pressed, hovered });
+ }
+ return style;
+ },
+ [style],
+ );
+
+ return (
+
+
+ {children}
+ {chevron !== null &&
+ (chevron ?? (
+
+
+
+ ))}
+
+
+ );
+});
+
+const styles = StyleSheet.create((theme) => ({
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[1],
+ },
+ chevronContainer: {
+ transform: [{ translateY: 1 }],
+ },
+}));
diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx
index 959f5fe82..bb77294ea 100644
--- a/packages/app/src/components/ui/dropdown-menu.tsx
+++ b/packages/app/src/components/ui/dropdown-menu.tsx
@@ -303,7 +303,7 @@ interface TriggerState {
}
type TriggerStyleProp = StyleProp | ((state: TriggerState) => StyleProp);
-interface DropdownMenuTriggerProps extends Omit {
+export interface DropdownMenuTriggerProps extends Omit {
style?: TriggerStyleProp;
children: ReactNode | ((state: TriggerState) => ReactNode);
}
diff --git a/packages/app/src/components/ui/dropdown-trigger.tsx b/packages/app/src/components/ui/dropdown-trigger.tsx
new file mode 100644
index 000000000..8c88ca84a
--- /dev/null
+++ b/packages/app/src/components/ui/dropdown-trigger.tsx
@@ -0,0 +1,48 @@
+import type { ReactElement, ReactNode } from "react";
+import { View } from "react-native";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
+import { ChevronDown } from "lucide-react-native";
+import { ICON_SIZE, type Theme } from "@/styles/theme";
+import { DropdownMenuTrigger, type DropdownMenuTriggerProps } from "@/components/ui/dropdown-menu";
+
+const ThemedChevronDown = withUnistyles(ChevronDown);
+
+interface DropdownTriggerProps extends Omit {
+ children?: ReactNode;
+ chevron?: ReactNode | null;
+}
+
+const chevronColorMapping = (theme: Theme) => ({
+ color: theme.colors.foregroundMuted,
+});
+
+export function DropdownTrigger({
+ children,
+ chevron,
+ ...props
+}: DropdownTriggerProps): ReactElement {
+ return (
+
+
+ {children}
+ {chevron !== null &&
+ (chevron ?? (
+
+
+
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[1],
+ },
+ chevronContainer: {
+ transform: [{ translateY: 1 }],
+ },
+}));
diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx
index 0680b9414..f771df5c1 100644
--- a/packages/app/src/composer/agent-controls/index.tsx
+++ b/packages/app/src/composer/agent-controls/index.tsx
@@ -20,7 +20,9 @@ import {
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
-import { Brain, ChevronDown, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
+import { Brain, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
+import { DropdownTrigger } from "@/components/ui/dropdown-trigger";
+import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import {
@@ -37,12 +39,7 @@ import {
toggleFavoriteModel,
useFormPreferences,
} from "@/hooks/use-form-preferences";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { DraftAgentModeControl, AgentModeControl } from "@/composer/agent-controls/mode-control";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
@@ -764,7 +761,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
<>
{providerOptions && providerOptions.length > 0 ? (
<>
-
{displayProvider}
-
-
+
-
{displayThinking}
-
-
+
{t(getAgentControlHintKey("thinking"))}
@@ -1173,7 +1168,7 @@ function DesktopFeatureItem({
-
{selectedOption?.label ?? feature.label}
-
-
+
{getFeatureTooltip(feature)}
@@ -1288,7 +1282,7 @@ function SheetFeatureItem({
open={openSelector === featureSelector}
onOpenChange={handleFeatureOpenChange}
>
-
{selectedOption?.label ?? feature.label}
-
-
+
{feature.options.map((option) => (
-
{Icon ? : null}
{selectedModeLabel}
-
-
+
{triggerLabel}
-
>
);
}
@@ -197,7 +197,7 @@ function RefPickerTrigger({
return (
-
-
+
{tooltipLabel}
@@ -247,7 +247,7 @@ function ProjectPickerTrigger({
return (
-
{label}
-
-
+
Choose project
@@ -652,7 +651,7 @@ function IsolationPickerTrigger({
iconSize: number;
}) {
return (
-
{label}
-
-
+
);
}
-// Each row aligns its label glyph with the screen heading glyph and lets the
-// label's natural width push the control. The label and control sit side by
-// side with no held horizontal space.
-function LabeledRow({ label, children }: { label: string; children: React.ReactNode }) {
- return (
-
- {label}
- {children}
-
- );
+// Wraps a single argument control in the mobile vertical stack. On desktop the
+// controls are laid out in one horizontal row, so no per-control wrapper is used.
+function FormRow({ children }: { children: React.ReactNode }) {
+ return {children};
}
interface WorkspaceBackingState {
@@ -1533,155 +1525,167 @@ export function NewWorkspaceScreen({
const backingTriggerLabel = backingLabel(t, effectiveBacking);
- const formStack = useMemo(
- () => (
-
-
-
-
-
-
-
- {/* The Isolation row keeps its height for non-git projects so switching
- projects never shifts the form; worktree backing is git-only, so a
- non-git project renders an invisible spacer matching the trigger
- height exactly. */}
- {canCreateWorktree ? (
-
-
-
-
-
-
- ) : (
-
- )}
- {/* The Base row keeps its height so toggling Isolation never shifts the
- form; on Local backing it renders an invisible spacer with no label
- or control, matching the trigger height exactly. */}
- {showRefPicker ? (
-
-
-
-
-
-
- ) : (
-
- )}
+ const formStack = useMemo(() => {
+ const projectControl = (
+
+
+
- ),
- [
- backingOptions,
- backingPickerOpen,
- backingTriggerLabel,
- badgePressableStyle,
- canCreateWorktree,
- effectiveBacking,
- handleBackingPickerOpenChange,
- handlePickerOpenChange,
- handleProjectPickerOpenChange,
- handleSelectBackingOption,
- handleSelectOption,
- handleSelectProjectOption,
- isPending,
- openBackingPicker,
- openPicker,
- openProjectPicker,
- options,
- pickerEmptyText,
- pickerOpen,
- projectPickerOpen,
- projectPickerOptions,
- projectTriggerLabel,
- projectIconDataByProjectKey,
- renderBackingOption,
- renderPickerOption,
- renderProjectOption,
- selectedItem,
- selectedOptionId,
- selectedProject,
- selectedProjectOptionId,
- selectedSourceDirectory,
- setPickerSearchQuery,
- showRefPicker,
- t,
- theme.colors.foregroundMuted,
- theme.iconSize.sm,
- triggerLabel,
- ],
- );
+ );
+
+ const isolationControl = canCreateWorktree ? (
+
+
+
+
+ ) : null;
+
+ const baseControl = showRefPicker ? (
+
+
+
+
+ ) : null;
+
+ if (isCompact) {
+ return (
+
+ {projectControl}
+ {/* The Isolation row keeps its height for non-git projects so switching
+ projects never shifts the form; worktree backing is git-only, so a
+ non-git project renders an invisible spacer matching the trigger
+ height exactly. */}
+ {isolationControl ? (
+ {isolationControl}
+ ) : (
+
+ )}
+ {/* The Base row keeps its height so toggling Isolation never shifts the
+ form; on Local backing it renders an invisible spacer with no label
+ or control, matching the trigger height exactly. */}
+ {baseControl ? {baseControl} : }
+
+ );
+ }
+
+ return (
+
+ {projectControl}
+ {isolationControl}
+ {baseControl}
+
+ );
+ }, [
+ backingOptions,
+ backingPickerOpen,
+ backingTriggerLabel,
+ badgePressableStyle,
+ canCreateWorktree,
+ effectiveBacking,
+ handleBackingPickerOpenChange,
+ handlePickerOpenChange,
+ handleProjectPickerOpenChange,
+ handleSelectBackingOption,
+ handleSelectOption,
+ handleSelectProjectOption,
+ isCompact,
+ isPending,
+ openBackingPicker,
+ openPicker,
+ openProjectPicker,
+ options,
+ pickerEmptyText,
+ pickerOpen,
+ projectIconDataByProjectKey,
+ projectPickerOpen,
+ projectPickerOptions,
+ projectTriggerLabel,
+ renderBackingOption,
+ renderPickerOption,
+ renderProjectOption,
+ selectedItem,
+ selectedOptionId,
+ selectedProject,
+ selectedProjectOptionId,
+ selectedSourceDirectory,
+ setPickerSearchQuery,
+ showRefPicker,
+ t,
+ theme.colors.foregroundMuted,
+ theme.iconSize.sm,
+ triggerLabel,
+ ]);
const composerFooter = useMemo(
() => (
@@ -1804,19 +1808,24 @@ const styles = StyleSheet.create((theme) => ({
marginBottom: theme.spacing[8],
gap: theme.spacing[2],
},
+ formStackDesktop: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: theme.spacing[8],
+ // The badge adds its own left padding; offset it so the project icon's left
+ // edge lands exactly on the "New workspace" title's left edge.
+ paddingLeft: theme.spacing[4],
+ gap: theme.spacing[2],
+ },
// The row's left inset matches the heading's text x (composerTitleContainer
- // paddingLeft) so each label glyph aligns with the "New workspace" glyph. The
- // label keeps its natural width and the control sits immediately beside it.
+ // paddingLeft) so the control aligns with the "New workspace" glyph. The badge
+ // adds its own left padding, so the row inset is reduced by that amount.
row: {
flexDirection: "row",
alignItems: "center",
- paddingLeft: theme.spacing[6],
+ paddingLeft: theme.spacing[4],
gap: theme.spacing[1],
},
- rowLabel: {
- fontSize: theme.fontSize.sm,
- color: theme.colors.foregroundMuted,
- },
baseSpacer: {
height: BADGE_HEIGHT,
},
diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx
index 50151bb76..c50bae368 100644
--- a/packages/app/src/screens/settings-screen.tsx
+++ b/packages/app/src/screens/settings-screen.tsx
@@ -26,7 +26,6 @@ import type { TFunction } from "i18next";
import { Buffer } from "buffer";
import {
ArrowLeft,
- ChevronDown,
Monitor,
Settings,
Palette,
@@ -43,6 +42,8 @@ import {
FolderGit2,
SquareTerminal,
} from "lucide-react-native";
+import { DropdownTrigger } from "@/components/ui/dropdown-trigger";
+import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
import { SidebarSeparator } from "@/components/sidebar/sidebar-separator";
import { ScreenTitle } from "@/components/headers/screen-title";
@@ -78,12 +79,7 @@ import { KeyboardShortcutsSection } from "@/screens/settings/keyboard-shortcuts-
import { Button } from "@/components/ui/button";
import { CommunityLinks } from "@/components/community-links";
import { SegmentedControl } from "@/components/ui/segmented-control";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { IntegrationsSection } from "@/desktop/components/integrations-section";
@@ -312,10 +308,8 @@ function GeneralSection({
handleLanguageChange,
handleTerminalScrollbackLinesChange,
}: GeneralSectionProps) {
- const { theme } = useUnistyles();
const { t, i18n } = useTranslation();
const activeLocale = getActiveLocale(i18n.language);
- const iconColor = theme.colors.foregroundMuted;
const sendBehaviorOptions = useMemo(() => getSendBehaviorOptions(t), [t]);
const sendBehaviorDescriptionKey =
settings.sendBehavior === "interrupt"
@@ -377,14 +371,13 @@ function GeneralSection({
{t("settings.general.language.description")}
-
{selectedLanguageLabel}
-
-
+
{LANGUAGE_OPTIONS.map((option) => (
-
+
{getServiceUrlBehaviorLabel(t, settings.serviceUrlBehavior)}
-
-
+
{SERVICE_URL_BEHAVIOR_VALUES.map((value) => (
-
{activeHost?.label ?? t("settings.groups.host")}
-
-
+