Update files

This commit is contained in:
Mohamed Boudra
2026-02-06 23:42:13 +07:00
parent 1a01e836bc
commit 8694696f45
8 changed files with 488 additions and 292 deletions
@@ -8,6 +8,7 @@ import {
ActivityIndicator,
Platform,
} from "react-native";
import type { StyleProp, ViewStyle, TextProps } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import {
BottomSheetModal,
@@ -16,7 +17,7 @@ import {
BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet";
import Animated from "react-native-reanimated";
import { ChevronDown, ChevronRight, Pencil, Check, X } from "lucide-react-native";
import { ChevronDown, ChevronRight, Pencil, Check, X, Bot, Brain, Shield } from "lucide-react-native";
import { theme as defaultTheme } from "@/styles/theme";
import type {
AgentMode,
@@ -312,6 +313,8 @@ interface ComboSelectProps {
allowCustomValue?: boolean;
isLoading?: boolean;
onSelect: (id: string) => void;
icon?: ReactElement;
showLabel?: boolean;
}
export function ComboSelect({
@@ -324,6 +327,8 @@ export function ComboSelect({
allowCustomValue = false,
isLoading,
onSelect,
icon,
showLabel = true,
}: ComboSelectProps): ReactElement {
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null);
@@ -336,7 +341,7 @@ export function ComboSelect({
return (
<>
<CompactSelectField
<FormSelectTrigger
label={label}
value={displayValue}
placeholder={placeholder}
@@ -344,6 +349,8 @@ export function ComboSelect({
disabled={disabled}
isLoading={isLoading}
controlRef={anchorRef}
icon={icon}
showLabel={showLabel}
/>
<Combobox
options={options}
@@ -368,9 +375,14 @@ interface CompactSelectFieldProps {
disabled?: boolean;
isLoading?: boolean;
controlRef?: React.RefObject<View | null>;
icon?: ReactElement;
showLabel?: boolean;
containerStyle?: StyleProp<ViewStyle>;
valueEllipsizeMode?: TextProps["ellipsizeMode"];
testID?: string;
}
function CompactSelectField({
export function FormSelectTrigger({
label,
value,
placeholder,
@@ -378,6 +390,11 @@ function CompactSelectField({
disabled,
isLoading,
controlRef,
icon,
showLabel = true,
containerStyle,
valueEllipsizeMode,
testID,
}: CompactSelectFieldProps): ReactElement {
const getWebKey = useCallback((event: unknown): string | null => {
if (!event || typeof event !== "object") return null;
@@ -411,25 +428,38 @@ function CompactSelectField({
<Pressable
ref={controlRef}
onPress={onPress}
testID={testID}
// @ts-ignore - tabIndex is web-only
tabIndex={0}
accessibilityRole="button"
// @ts-ignore - onKeyDown is web-only
onKeyDown={handleKeyDown}
disabled={disabled}
style={[styles.compactSelectControl, disabled && styles.compactSelectControlDisabled]}
style={[
styles.compactSelectControl,
!showLabel && styles.compactSelectControlInline,
containerStyle,
disabled && styles.compactSelectControlDisabled,
]}
>
<Text style={styles.compactSelectLabel}>{label}</Text>
{isLoading ? (
<ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} />
) : (
<Text
style={value ? styles.compactSelectValue : styles.compactSelectPlaceholder}
numberOfLines={1}
>
{value || placeholder || "Select..."}
</Text>
)}
{icon ? <View style={styles.compactSelectLeading}>{icon}</View> : null}
<View style={styles.compactSelectValueContainer}>
{showLabel ? (
<Text style={styles.compactSelectLabel}>{label}</Text>
) : null}
{isLoading ? (
<ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} />
) : (
<Text
style={value ? styles.compactSelectValue : styles.compactSelectPlaceholder}
numberOfLines={1}
ellipsizeMode={valueEllipsizeMode}
>
{value || placeholder || "Select..."}
</Text>
)}
</View>
<ChevronDown size={16} color={defaultTheme.colors.foregroundMuted} />
</Pressable>
);
}
@@ -466,31 +496,28 @@ export function AgentConfigRow({
providerDefinitions.map((def) => ({
id: def.id,
label: def.label,
description: def.description,
})),
[providerDefinitions]
);
const modeSelectOptions: ComboSelectOption[] = useMemo(() => {
if (modeOptions.length === 0) {
return [{ id: "", label: "Default", description: "Provider default mode" }];
return [{ id: "", label: "Default" }];
}
return modeOptions.map((mode) => ({
id: mode.id,
label: mode.label,
description: mode.description,
}));
}, [modeOptions]);
const modelSelectOptions: ComboSelectOption[] = useMemo(() => {
const opts: ComboSelectOption[] = [
{ id: "", label: "Auto", description: "Provider default model" },
{ id: "", label: "Auto" },
];
for (const model of models) {
opts.push({
id: model.id,
label: model.label,
description: model.description,
});
}
return opts;
@@ -502,18 +529,20 @@ export function AgentConfigRow({
<View style={styles.agentConfigRow}>
<View style={styles.agentConfigColumn}>
<ComboSelect
label="PROVIDER"
label="Provider"
title="Select provider"
value={selectedProvider}
options={providerOptions}
placeholder="Select..."
disabled={disabled}
onSelect={onSelectProvider}
icon={<Bot size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/>
</View>
<View style={styles.agentConfigColumn}>
<ComboSelect
label="MODEL"
label="Model"
title="Select model"
value={selectedModel}
options={modelSelectOptions}
@@ -521,17 +550,21 @@ export function AgentConfigRow({
disabled={disabled}
isLoading={isModelLoading}
onSelect={onSelectModel}
icon={<Brain size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/>
</View>
<View style={styles.agentConfigColumn}>
<ComboSelect
label="MODE"
label="Mode"
title="Select mode"
value={effectiveSelectedMode}
options={modeSelectOptions}
placeholder="Default"
disabled={disabled || modeOptions.length === 0}
onSelect={onSelectMode}
icon={<Shield size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/>
</View>
</View>
@@ -563,7 +596,6 @@ export function AssistantDropdown({
providerDefinitions.map((def) => ({
id: def.id,
label: def.label,
description: def.description,
})),
[providerDefinitions]
);
@@ -1405,26 +1437,46 @@ const styles = StyleSheet.create((theme) => ({
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.lg,
paddingVertical: theme.spacing[2],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[3],
gap: theme.spacing[1],
},
compactSelectControlInline: {
minHeight: 40,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[1],
},
compactSelectControlDisabled: {
opacity: theme.opacity[50],
},
compactSelectTopRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
compactSelectLeading: {
alignItems: "center",
justifyContent: "center",
marginRight: theme.spacing[1],
},
compactSelectValueContainer: {
flex: 1,
minWidth: 0,
gap: theme.spacing[1],
},
compactSelectLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
textTransform: "uppercase",
letterSpacing: 0.5,
},
compactSelectValue: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontSize: theme.fontSize.base,
},
compactSelectPlaceholder: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontSize: theme.fontSize.base,
},
}));
@@ -6,7 +6,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
@@ -67,7 +66,9 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
return models.find((m) => m.id === agent.model) ?? null;
}, [models, agent.model]);
const displayModel = selectedModel?.label ?? agent.model ?? "default";
const displayModel = selectedModel
? selectedModel.label
: agent.model ?? "default";
const thinkingOptions = selectedModel?.thinkingOptions ?? null;
const selectedThinkingId =
@@ -106,14 +107,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth}
testID="agent-mode-menu"
>
<DropdownMenuLabel>Mode</DropdownMenuLabel>
{agent.availableModes.map((mode) => {
const isActive = mode.id === agent.currentModeId;
return (
<DropdownMenuItem
key={mode.id}
selected={isActive}
description={mode.description}
onSelect={() => handleModeChange(mode.id)}
>
{mode.label}
@@ -147,14 +146,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth}
testID="agent-model-menu"
>
<DropdownMenuLabel>Model</DropdownMenuLabel>
{models?.map((model) => {
const isActive = model.id === agent.model;
return (
<DropdownMenuItem
key={model.id}
selected={isActive}
description={model.description}
onSelect={() => {
if (!client) {
return;
@@ -193,14 +190,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth}
testID="agent-thinking-menu"
>
<DropdownMenuLabel>Thinking</DropdownMenuLabel>
{thinkingOptions.map((opt) => {
const isActive = opt.id === selectedThinkingId;
return (
<DropdownMenuItem
key={opt.id}
selected={isActive}
description={opt.description}
onSelect={() => {
if (!client) {
return;
@@ -245,7 +240,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
testID="agent-preferences-sheet"
>
<View style={styles.sheetSection}>
<Text style={styles.sheetLabel}>Model</Text>
<DropdownMenu>
<DropdownMenuTrigger
style={({ pressed }) => [
@@ -260,14 +254,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuLabel>Model</DropdownMenuLabel>
{models?.map((model) => {
const isActive = model.id === agent.model;
return (
<DropdownMenuItem
key={model.id}
selected={isActive}
description={model.description}
onSelect={() => {
if (!client) {
return;
@@ -287,7 +279,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
{thinkingOptions && thinkingOptions.length > 1 && (
<View style={styles.sheetSection}>
<Text style={styles.sheetLabel}>Thinking</Text>
<DropdownMenu>
<DropdownMenuTrigger
style={({ pressed }) => [
@@ -298,21 +289,19 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
accessibilityLabel="Select thinking option"
testID="agent-preferences-thinking"
>
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuLabel>Thinking</DropdownMenuLabel>
{thinkingOptions.map((opt) => {
const isActive = opt.id === selectedThinkingId;
return (
<DropdownMenuItem
key={opt.id}
selected={isActive}
description={opt.description}
onSelect={() => {
if (!client) {
return;
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
{thinkingOptions.map((opt) => {
const isActive = opt.id === selectedThinkingId;
return (
<DropdownMenuItem
key={opt.id}
selected={isActive}
onSelect={() => {
if (!client) {
return;
}
void client
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
@@ -361,7 +350,7 @@ const styles = StyleSheet.create((theme) => ({
},
modeBadgeText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
prefsButton: {
@@ -378,11 +367,6 @@ const styles = StyleSheet.create((theme) => ({
sheetSection: {
gap: theme.spacing[2],
},
sheetLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
sheetSelect: {
flexDirection: "row",
alignItems: "center",
@@ -401,7 +385,7 @@ const styles = StyleSheet.create((theme) => ({
sheetSelectText: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
},
}));
+30 -11
View File
@@ -1050,14 +1050,24 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
accessibilityRole="button"
accessibilityLabel={gitActions.primary.label}
>
{gitActions.primary.status === "pending" ? (
<ActivityIndicator size="small" color={theme.colors.foreground} style={styles.splitButtonSpinner} />
) : (
<View style={styles.splitButtonContent}>
<View style={styles.splitButtonPrimaryInner}>
<View
style={[
styles.splitButtonContent,
gitActions.primary.status === "pending" && styles.splitButtonContentHidden,
]}
>
{gitActions.primary.icon}
<Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text>
</View>
)}
{gitActions.primary.status === "pending" ? (
<ActivityIndicator
size="small"
color={theme.colors.foreground}
style={styles.splitButtonSpinnerOverlay}
/>
) : null}
</View>
</Pressable>
{gitActions.secondary.length > 0 ? (
<DropdownMenu>
@@ -1292,15 +1302,21 @@ const styles = StyleSheet.create((theme) => ({
},
splitButtonPrimary: {
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
paddingVertical: theme.spacing[1],
justifyContent: "center",
position: "relative",
},
splitButtonPrimaryInner: {
position: "relative",
alignItems: "center",
justifyContent: "center",
},
splitButtonPrimaryDisabled: {
opacity: 0.6,
},
splitButtonText: {
fontSize: theme.fontSize.xs,
lineHeight: theme.fontSize.xs * 1.5,
fontSize: theme.fontSize.sm,
lineHeight: theme.fontSize.sm * 1.5,
color: theme.colors.foreground,
fontWeight: theme.fontWeight.medium,
},
@@ -1310,9 +1326,12 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "center",
gap: theme.spacing[2],
},
splitButtonSpinner: {
height: theme.fontSize.xs * 1.5,
width: theme.fontSize.xs * 1.5,
splitButtonContentHidden: {
opacity: 0,
},
splitButtonSpinnerOverlay: {
position: "absolute",
transform: [{ scale: 0.8 }],
},
splitButtonCaret: {
width: 36,
+37 -25
View File
@@ -20,13 +20,7 @@ import {
} from "@gorhom/bottom-sheet";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { Check, Search } from "lucide-react-native";
import {
flip,
offset as floatingOffset,
shift,
size as floatingSize,
useFloating,
} from "@floating-ui/react-native";
import { flip, offset as floatingOffset, shift, size as floatingSize, useFloating } from "@floating-ui/react-native";
import { getNextActiveIndex } from "./combobox-keyboard";
const IS_WEB = Platform.OS === "web";
@@ -132,21 +126,24 @@ export function ComboboxItem({
<Pressable
testID={testID}
onPress={onPress}
style={({ pressed }) => [
style={({ pressed, hovered = false }) => [
styles.comboboxItem,
hovered && styles.comboboxItemHovered,
pressed && styles.comboboxItemPressed,
active && styles.comboboxItemActive,
]}
>
<View style={styles.comboboxItemCheckSlot}>
{selected ? <Check size={16} color={theme.colors.foreground} /> : null}
</View>
<View style={styles.comboboxItemContent}>
<Text numberOfLines={1} style={styles.comboboxItemLabel}>{label}</Text>
{description ? (
<Text numberOfLines={2} style={styles.comboboxItemDescription}>{description}</Text>
) : null}
</View>
{selected ? (
<View style={styles.comboboxItemTrailingSlot}>
<Check size={16} color={theme.colors.foregroundMuted} />
</View>
) : null}
</Pressable>
);
}
@@ -214,8 +211,8 @@ export function Combobox({
const middleware = useMemo(
() => [
floatingOffset({ mainAxis: 4 }),
flip({ padding: collisionPadding }),
floatingOffset(Platform.OS === "web" ? 0 : 4),
...(Platform.OS === "web" ? [] : [flip({ padding: collisionPadding })]),
shift({ padding: collisionPadding }),
floatingSize({
padding: collisionPadding,
@@ -238,7 +235,7 @@ export function Combobox({
);
const { refs, floatingStyles, update } = useFloating({
placement: "bottom-start",
placement: Platform.OS === "web" ? "top-start" : "bottom-start",
middleware,
sameScrollView: false,
elements: {
@@ -551,10 +548,14 @@ const styles = StyleSheet.create((theme) => ({
searchInputContainer: {
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface1,
borderRadius: theme.borderRadius.lg,
paddingHorizontal: theme.spacing[3],
marginBottom: theme.spacing[1],
marginHorizontal: theme.spacing[2],
marginBottom: theme.spacing[2],
marginTop: theme.spacing[1],
gap: theme.spacing[2],
},
searchInput: {
@@ -571,17 +572,27 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.md,
...(IS_WEB
? {}
: {
marginHorizontal: theme.spacing[1],
marginBottom: theme.spacing[1],
}),
},
comboboxItemHovered: {
backgroundColor: theme.colors.surface1,
},
comboboxItemPressed: {
backgroundColor: theme.colors.surface2,
backgroundColor: theme.colors.surface1,
},
comboboxItemActive: {
backgroundColor: theme.colors.surface2,
backgroundColor: theme.colors.surface1,
},
comboboxItemCheckSlot: {
comboboxItemTrailingSlot: {
width: 16,
alignItems: "center",
justifyContent: "center",
marginLeft: "auto",
},
comboboxItemContent: {
flex: 1,
@@ -603,7 +614,7 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
bottomSheetBackground: {
backgroundColor: theme.colors.surface2,
backgroundColor: theme.colors.surface0,
borderTopLeftRadius: theme.borderRadius["2xl"],
borderTopRightRadius: theme.borderRadius["2xl"],
},
@@ -616,13 +627,14 @@ const styles = StyleSheet.create((theme) => ({
},
comboboxTitle: {
fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.semibold,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
textAlign: "center",
textAlign: "left",
},
comboboxScrollContent: {
paddingBottom: theme.spacing[8],
paddingHorizontal: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[1],
},
desktopOverlay: {
flex: 1,
@@ -638,7 +650,7 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface0,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.borderAccent,
borderColor: theme.colors.border,
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
@@ -458,6 +458,9 @@ export function DropdownMenuItem({
label = successLabel;
}
const trailingContent =
trailing ?? (!showSelectedCheck && selected ? <Check size={16} color={theme.colors.foregroundMuted} /> : null);
return (
<Pressable
testID={testID}
@@ -473,8 +476,12 @@ export function DropdownMenuItem({
style={({ pressed, hovered }) => [
styles.item,
selected ? (selectedVariant === "accent" ? styles.itemSelectedAccent : styles.itemSelected) : null,
selected && (hovered || pressed) && selectedVariant !== "accent"
? styles.itemSelectedInteractive
: null,
isDisabled ? styles.itemDisabled : null,
(hovered || pressed) && !isDisabled ? styles.itemHovered : null,
hovered && !pressed && !isDisabled ? styles.itemHovered : null,
pressed && !isDisabled ? styles.itemPressed : null,
]}
>
{showSelectedCheck ? (
@@ -507,7 +514,7 @@ export function DropdownMenuItem({
</Text>
) : null}
</View>
{trailing ? <View style={styles.trailingSlot}>{trailing}</View> : null}
{trailingContent ? <View style={styles.trailingSlot}>{trailingContent}</View> : null}
</Pressable>
);
}
@@ -565,12 +572,20 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
borderWidth: theme.borderWidth[1],
borderColor: "transparent",
},
itemHovered: {
backgroundColor: theme.colors.surface2,
backgroundColor: theme.colors.surface1,
},
itemPressed: {
backgroundColor: theme.colors.surface1,
},
itemSelected: {
backgroundColor: theme.colors.surface2,
backgroundColor: theme.colors.surface1,
},
itemSelectedInteractive: {
backgroundColor: theme.colors.surface1,
},
itemSelectedAccent: {
backgroundColor: theme.colors.accent,