Scoped to only `no-unused-vars` and `react/jsx-no-useless-fragment` — the safe subset of --fix-suggestions. Primary changes: - Remove unused named imports from statements - Remove unused default/type imports entirely - Collapse `<><Single/></>` fragments to `<Single/>` Verified clean: typecheck + format + lint all green.
1520 lines
42 KiB
TypeScript
1520 lines
42 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import type { ReactElement, ReactNode } from "react";
|
|
import { View, Text, Pressable, TextInput, ActivityIndicator } from "react-native";
|
|
import type { StyleProp, ViewStyle, TextProps } from "react-native";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import {
|
|
BottomSheetScrollView,
|
|
BottomSheetBackdrop,
|
|
BottomSheetBackgroundProps,
|
|
} from "@gorhom/bottom-sheet";
|
|
import Animated from "react-native-reanimated";
|
|
import {
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Pencil,
|
|
Check,
|
|
X,
|
|
Bot,
|
|
Brain,
|
|
ShieldCheck,
|
|
ShieldAlert,
|
|
ShieldOff,
|
|
} from "lucide-react-native";
|
|
import type {
|
|
AgentMode,
|
|
AgentModelDefinition,
|
|
AgentProvider,
|
|
} from "@server/server/agent/agent-sdk-types";
|
|
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
|
import { getModeVisuals, type AgentModeIcon } from "@server/server/agent/provider-manifest";
|
|
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
|
import {
|
|
IsolatedBottomSheetModal,
|
|
useIsolatedBottomSheetVisibility,
|
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
|
import { baseColors } from "@/styles/theme";
|
|
import { isNative } from "@/constants/platform";
|
|
|
|
const MODE_ICON_MAP: Record<AgentModeIcon, typeof ShieldCheck> = {
|
|
ShieldCheck,
|
|
ShieldAlert,
|
|
ShieldOff,
|
|
};
|
|
|
|
const MODE_COLOR_MAP: Record<string, string> = {
|
|
default: baseColors.blue[500],
|
|
safe: baseColors.green[500],
|
|
moderate: baseColors.amber[500],
|
|
dangerous: baseColors.red[500],
|
|
readonly: baseColors.purple[500],
|
|
};
|
|
|
|
type DropdownTriggerRenderProps = {
|
|
label: string;
|
|
value: string;
|
|
placeholder: string;
|
|
onPress: () => void;
|
|
disabled?: boolean;
|
|
errorMessage?: string | null;
|
|
warningMessage?: string | null;
|
|
helperText?: string | null;
|
|
};
|
|
|
|
type DropdownTriggerRenderer = (props: DropdownTriggerRenderProps) => ReactNode;
|
|
|
|
interface DropdownFieldProps {
|
|
label: string;
|
|
value: string;
|
|
placeholder: string;
|
|
onPress: () => void;
|
|
disabled?: boolean;
|
|
errorMessage?: string | null;
|
|
warningMessage?: string | null;
|
|
helperText?: string | null;
|
|
renderTrigger?: DropdownTriggerRenderer;
|
|
testID?: string;
|
|
}
|
|
|
|
export function DropdownField({
|
|
label,
|
|
value,
|
|
placeholder,
|
|
onPress,
|
|
disabled,
|
|
errorMessage,
|
|
warningMessage,
|
|
helperText,
|
|
renderTrigger,
|
|
testID,
|
|
}: DropdownFieldProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
|
|
if (renderTrigger) {
|
|
return (
|
|
<>
|
|
{renderTrigger({
|
|
label,
|
|
value,
|
|
placeholder,
|
|
onPress,
|
|
disabled,
|
|
errorMessage,
|
|
warningMessage,
|
|
helperText,
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.formSection}>
|
|
<Text style={styles.label}>{label}</Text>
|
|
<Pressable
|
|
onPress={onPress}
|
|
disabled={disabled}
|
|
testID={testID}
|
|
style={[styles.dropdownControl, disabled && styles.dropdownControlDisabled]}
|
|
>
|
|
<Text style={value ? styles.dropdownValue : styles.dropdownPlaceholder} numberOfLines={1}>
|
|
{value || placeholder}
|
|
</Text>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
|
{!errorMessage && helperText ? <Text style={styles.helperText}>{helperText}</Text> : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface SelectFieldProps {
|
|
label: string;
|
|
value: string;
|
|
placeholder?: string;
|
|
onPress: () => void;
|
|
disabled?: boolean;
|
|
errorMessage?: string | null;
|
|
warningMessage?: string | null;
|
|
helperText?: string | null;
|
|
controlRef?: React.RefObject<View | null>;
|
|
valueEllipsizeMode?: "head" | "middle" | "tail" | "clip";
|
|
testID?: string;
|
|
}
|
|
|
|
export function SelectField({
|
|
label,
|
|
value,
|
|
placeholder,
|
|
onPress,
|
|
disabled,
|
|
errorMessage,
|
|
warningMessage,
|
|
helperText,
|
|
controlRef,
|
|
valueEllipsizeMode,
|
|
testID,
|
|
}: SelectFieldProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
|
|
const getWebKey = useCallback((event: unknown): string | null => {
|
|
if (!event || typeof event !== "object") return null;
|
|
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
|
if (typeof eventWithNative.key === "string") return eventWithNative.key;
|
|
const nativeEvent = eventWithNative.nativeEvent as { key?: unknown } | undefined;
|
|
return typeof nativeEvent?.key === "string" ? nativeEvent.key : null;
|
|
}, []);
|
|
|
|
const preventWebDefault = useCallback((event: unknown) => {
|
|
if (!event || typeof event !== "object") return;
|
|
const candidate = event as { preventDefault?: unknown };
|
|
if (typeof candidate.preventDefault === "function") {
|
|
candidate.preventDefault();
|
|
}
|
|
}, []);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: unknown) => {
|
|
if (isNative) return;
|
|
const key = getWebKey(event);
|
|
if (key === "Enter" || key === " ") {
|
|
preventWebDefault(event);
|
|
onPress();
|
|
}
|
|
},
|
|
[getWebKey, onPress, preventWebDefault],
|
|
);
|
|
|
|
const normalizedValue = (value ?? "").trim();
|
|
const normalizedPlaceholder = (placeholder ?? "").trim();
|
|
const hasConcreteValue =
|
|
normalizedValue.length > 0 &&
|
|
(normalizedPlaceholder.length === 0 || normalizedValue !== normalizedPlaceholder);
|
|
const displayText = hasConcreteValue ? normalizedValue : normalizedPlaceholder || "Select...";
|
|
|
|
return (
|
|
<View style={styles.selectFieldContainer}>
|
|
<Pressable
|
|
ref={controlRef}
|
|
onPress={onPress}
|
|
// @ts-ignore - tabIndex is web-only
|
|
tabIndex={0}
|
|
accessibilityRole="button"
|
|
// @ts-ignore - onKeyDown is web-only
|
|
onKeyDown={handleKeyDown}
|
|
disabled={disabled}
|
|
testID={testID}
|
|
style={[styles.selectFieldControl, disabled && styles.selectFieldControlDisabled]}
|
|
>
|
|
<View style={styles.selectFieldContent}>
|
|
<Text style={styles.selectFieldLabel}>{label}</Text>
|
|
<Text
|
|
style={value ? styles.selectFieldValue : styles.selectFieldPlaceholder}
|
|
numberOfLines={1}
|
|
ellipsizeMode={valueEllipsizeMode ?? "tail"}
|
|
>
|
|
{value || placeholder || "Select..."}
|
|
</Text>
|
|
</View>
|
|
<ChevronRight size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
|
{!errorMessage && !warningMessage && helperText ? (
|
|
<Text style={styles.helperText}>{helperText}</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface DropdownSheetProps {
|
|
title: string;
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
children: ReactNode;
|
|
}
|
|
|
|
function DropdownSheetBackground({ style }: BottomSheetBackgroundProps) {
|
|
const { theme } = useUnistyles();
|
|
|
|
return (
|
|
<Animated.View
|
|
pointerEvents="none"
|
|
style={[
|
|
style,
|
|
{
|
|
backgroundColor: theme.colors.surface2,
|
|
borderTopLeftRadius: theme.borderRadius["2xl"],
|
|
borderTopRightRadius: theme.borderRadius["2xl"],
|
|
},
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function DropdownSheet({
|
|
title,
|
|
visible,
|
|
onClose,
|
|
children,
|
|
}: DropdownSheetProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
const titleColor = theme.colors.foreground;
|
|
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
|
const { sheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
|
visible,
|
|
onClose,
|
|
});
|
|
|
|
const handleClose = useCallback(() => {
|
|
onClose();
|
|
}, [onClose]);
|
|
|
|
const renderBackdrop = useCallback(
|
|
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
|
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.45} />
|
|
),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<IsolatedBottomSheetModal
|
|
ref={sheetRef}
|
|
snapPoints={snapPoints}
|
|
index={0}
|
|
enableDynamicSizing={false}
|
|
onChange={handleSheetChange}
|
|
backdropComponent={renderBackdrop}
|
|
enablePanDownToClose
|
|
backgroundComponent={DropdownSheetBackground}
|
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
|
keyboardBehavior="extend"
|
|
keyboardBlurBehavior="restore"
|
|
>
|
|
<View style={styles.bottomSheetHeader}>
|
|
<Text key={titleColor} style={[styles.dropdownSheetTitle, { color: titleColor }]}>
|
|
{title}
|
|
</Text>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Close sheet"
|
|
onPress={handleClose}
|
|
hitSlop={10}
|
|
testID="dropdown-sheet-close"
|
|
>
|
|
<X size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
</View>
|
|
<BottomSheetScrollView
|
|
contentContainerStyle={styles.dropdownSheetScrollContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{children}
|
|
</BottomSheetScrollView>
|
|
</IsolatedBottomSheetModal>
|
|
);
|
|
}
|
|
|
|
// Re-export ComboboxItem as SelectOption for backwards compatibility
|
|
const SelectOption = ComboboxItem;
|
|
|
|
interface ComboSelectOption {
|
|
id: string;
|
|
label: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface ComboSelectProps {
|
|
label: string;
|
|
title: string;
|
|
value: string;
|
|
options: ComboSelectOption[];
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
allowCustomValue?: boolean;
|
|
isLoading?: boolean;
|
|
onSelect: (id: string) => void;
|
|
icon?: ReactElement;
|
|
showLabel?: boolean;
|
|
testID?: string;
|
|
}
|
|
|
|
export function ComboSelect({
|
|
label,
|
|
title,
|
|
value,
|
|
options,
|
|
placeholder,
|
|
disabled,
|
|
allowCustomValue = false,
|
|
isLoading,
|
|
onSelect,
|
|
icon,
|
|
showLabel = true,
|
|
testID,
|
|
}: ComboSelectProps): ReactElement {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const anchorRef = useRef<View>(null);
|
|
|
|
const selectedOption = options.find((opt) => opt.id === value);
|
|
const displayValue = selectedOption?.label ?? "";
|
|
const isEmpty = options.length === 0;
|
|
|
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
|
|
|
return (
|
|
<>
|
|
<FormSelectTrigger
|
|
label={label}
|
|
value={displayValue}
|
|
placeholder={placeholder}
|
|
onPress={handleOpen}
|
|
disabled={disabled || isEmpty}
|
|
isLoading={isLoading}
|
|
controlRef={anchorRef}
|
|
icon={icon}
|
|
showLabel={showLabel}
|
|
testID={testID}
|
|
/>
|
|
<Combobox
|
|
options={options}
|
|
value={value}
|
|
onSelect={onSelect}
|
|
searchPlaceholder={`Search ${label.toLowerCase()}...`}
|
|
allowCustomValue={allowCustomValue}
|
|
title={title}
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface CompactSelectFieldProps {
|
|
label: string;
|
|
value: string;
|
|
placeholder?: string;
|
|
onPress: () => void;
|
|
disabled?: boolean;
|
|
isLoading?: boolean;
|
|
controlRef?: React.RefObject<View | null>;
|
|
icon?: ReactElement;
|
|
showLabel?: boolean;
|
|
containerStyle?: StyleProp<ViewStyle>;
|
|
valueEllipsizeMode?: TextProps["ellipsizeMode"];
|
|
testID?: string;
|
|
}
|
|
|
|
export function FormSelectTrigger({
|
|
label,
|
|
value,
|
|
placeholder,
|
|
onPress,
|
|
disabled,
|
|
isLoading,
|
|
controlRef,
|
|
icon,
|
|
showLabel = true,
|
|
containerStyle,
|
|
valueEllipsizeMode,
|
|
testID,
|
|
}: CompactSelectFieldProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
|
|
const getWebKey = useCallback((event: unknown): string | null => {
|
|
if (!event || typeof event !== "object") return null;
|
|
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
|
if (typeof eventWithNative.key === "string") return eventWithNative.key;
|
|
const nativeEvent = eventWithNative.nativeEvent as { key?: unknown } | undefined;
|
|
return typeof nativeEvent?.key === "string" ? nativeEvent.key : null;
|
|
}, []);
|
|
|
|
const preventWebDefault = useCallback((event: unknown) => {
|
|
if (!event || typeof event !== "object") return;
|
|
const candidate = event as { preventDefault?: unknown };
|
|
if (typeof candidate.preventDefault === "function") {
|
|
candidate.preventDefault();
|
|
}
|
|
}, []);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: unknown) => {
|
|
if (isNative) return;
|
|
const key = getWebKey(event);
|
|
if (key === "Enter" || key === " ") {
|
|
preventWebDefault(event);
|
|
onPress();
|
|
}
|
|
},
|
|
[getWebKey, onPress, preventWebDefault],
|
|
);
|
|
const normalizedValue = (value ?? "").trim();
|
|
const normalizedPlaceholder = (placeholder ?? "").trim();
|
|
const hasConcreteValue =
|
|
normalizedValue.length > 0 &&
|
|
(normalizedPlaceholder.length === 0 || normalizedValue !== normalizedPlaceholder);
|
|
const displayText = hasConcreteValue ? normalizedValue : normalizedPlaceholder || "Select...";
|
|
|
|
return (
|
|
<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,
|
|
!showLabel && styles.compactSelectControlInline,
|
|
containerStyle,
|
|
disabled && styles.compactSelectControlDisabled,
|
|
]}
|
|
>
|
|
{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={theme.colors.foregroundMuted} />
|
|
) : (
|
|
<Text
|
|
style={hasConcreteValue ? styles.compactSelectValue : styles.compactSelectPlaceholder}
|
|
numberOfLines={1}
|
|
ellipsizeMode={valueEllipsizeMode}
|
|
>
|
|
{displayText}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
interface AgentConfigRowProps {
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
selectedProvider: AgentProvider;
|
|
onSelectProvider: (provider: AgentProvider) => void;
|
|
modeOptions: AgentMode[];
|
|
selectedMode: string;
|
|
onSelectMode: (modeId: string) => void;
|
|
models: AgentModelDefinition[];
|
|
selectedModel: string;
|
|
isModelLoading: boolean;
|
|
onSelectModel: (modelId: string) => void;
|
|
thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
|
|
selectedThinkingOptionId: string;
|
|
onSelectThinkingOption: (thinkingOptionId: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function AgentConfigRow({
|
|
providerDefinitions,
|
|
selectedProvider,
|
|
onSelectProvider,
|
|
modeOptions,
|
|
selectedMode,
|
|
onSelectMode,
|
|
models,
|
|
selectedModel,
|
|
isModelLoading,
|
|
onSelectModel,
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
onSelectThinkingOption,
|
|
disabled,
|
|
}: AgentConfigRowProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
|
|
const providerOptions: ComboSelectOption[] = useMemo(
|
|
() =>
|
|
providerDefinitions.map((def) => ({
|
|
id: def.id,
|
|
label: def.label,
|
|
})),
|
|
[providerDefinitions],
|
|
);
|
|
|
|
const modeSelectOptions: ComboSelectOption[] = useMemo(() => {
|
|
if (modeOptions.length === 0) {
|
|
return [{ id: "", label: "Default" }];
|
|
}
|
|
return modeOptions.map((mode) => ({
|
|
id: mode.id,
|
|
label: mode.label,
|
|
}));
|
|
}, [modeOptions]);
|
|
|
|
const modelSelectOptions: ComboSelectOption[] = useMemo(() => {
|
|
return models.map((model) => ({
|
|
id: model.id,
|
|
label: model.label,
|
|
}));
|
|
}, [models]);
|
|
|
|
const thinkingSelectOptions: ComboSelectOption[] = useMemo(
|
|
() =>
|
|
thinkingOptions.map((option) => ({
|
|
id: option.id,
|
|
label: option.label,
|
|
})),
|
|
[thinkingOptions],
|
|
);
|
|
|
|
const effectiveSelectedMode = selectedMode || (modeOptions.length > 0 ? modeOptions[0]?.id : "");
|
|
const effectiveSelectedThinkingOption =
|
|
selectedThinkingOptionId || thinkingSelectOptions[0]?.id || "";
|
|
|
|
const selectedModeVisuals = getModeVisuals(
|
|
selectedProvider,
|
|
effectiveSelectedMode,
|
|
providerDefinitions,
|
|
);
|
|
const ModeIcon = MODE_ICON_MAP[selectedModeVisuals?.icon ?? "ShieldCheck"];
|
|
const modeIconColor = MODE_COLOR_MAP[selectedModeVisuals?.colorTier ?? "safe"];
|
|
|
|
return (
|
|
<View style={styles.agentConfigRow}>
|
|
<View style={styles.agentConfigColumn}>
|
|
<ComboSelect
|
|
label="Provider"
|
|
title="Select provider"
|
|
value={selectedProvider}
|
|
options={providerOptions}
|
|
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
|
|
disabled={disabled || providerOptions.length === 0}
|
|
onSelect={onSelectProvider}
|
|
icon={<Bot size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
|
showLabel={false}
|
|
testID="draft-provider-select"
|
|
/>
|
|
</View>
|
|
<View style={styles.agentConfigColumn}>
|
|
<ComboSelect
|
|
label="Model"
|
|
title="Select model"
|
|
value={selectedModel}
|
|
options={modelSelectOptions}
|
|
placeholder={isModelLoading ? "Loading..." : "Select model"}
|
|
disabled={disabled}
|
|
isLoading={isModelLoading}
|
|
onSelect={onSelectModel}
|
|
icon={<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
|
showLabel={false}
|
|
testID="draft-model-select"
|
|
/>
|
|
</View>
|
|
<View style={styles.agentConfigColumn}>
|
|
<ComboSelect
|
|
label="Mode"
|
|
title="Select mode"
|
|
value={effectiveSelectedMode}
|
|
options={modeSelectOptions}
|
|
placeholder="Default"
|
|
disabled={disabled || modeOptions.length === 0}
|
|
onSelect={onSelectMode}
|
|
icon={<ModeIcon size={theme.iconSize.md} color={modeIconColor} />}
|
|
showLabel={false}
|
|
testID="draft-mode-select"
|
|
/>
|
|
</View>
|
|
{thinkingSelectOptions.length > 0 ? (
|
|
<View style={styles.agentConfigColumn}>
|
|
<ComboSelect
|
|
label="Thinking"
|
|
title="Select thinking effort"
|
|
value={effectiveSelectedThinkingOption}
|
|
options={thinkingSelectOptions}
|
|
placeholder="Select..."
|
|
disabled={disabled}
|
|
onSelect={onSelectThinkingOption}
|
|
icon={<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
|
showLabel={false}
|
|
/>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface AssistantDropdownProps {
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
selectedProvider: AgentProvider;
|
|
disabled: boolean;
|
|
onSelect: (provider: AgentProvider) => void;
|
|
}
|
|
|
|
export function AssistantDropdown({
|
|
providerDefinitions,
|
|
selectedProvider,
|
|
disabled,
|
|
onSelect,
|
|
}: AssistantDropdownProps): ReactElement {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const anchorRef = useRef<View>(null);
|
|
|
|
const selectedDefinition = providerDefinitions.find(
|
|
(definition) => definition.id === selectedProvider,
|
|
);
|
|
|
|
const options = useMemo(
|
|
() =>
|
|
providerDefinitions.map((def) => ({
|
|
id: def.id,
|
|
label: def.label,
|
|
})),
|
|
[providerDefinitions],
|
|
);
|
|
|
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
|
|
|
return (
|
|
<>
|
|
<SelectField
|
|
label="AGENT"
|
|
value={selectedDefinition?.label ?? ""}
|
|
placeholder="Select assistant"
|
|
onPress={handleOpen}
|
|
disabled={disabled}
|
|
controlRef={anchorRef}
|
|
/>
|
|
<Combobox
|
|
options={options}
|
|
value={selectedProvider}
|
|
onSelect={(id) => onSelect(id as AgentProvider)}
|
|
title="Choose assistant"
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface PermissionsDropdownProps {
|
|
modeOptions: AgentMode[];
|
|
selectedMode: string;
|
|
disabled: boolean;
|
|
onSelect: (modeId: string) => void;
|
|
}
|
|
|
|
export function PermissionsDropdown({
|
|
modeOptions,
|
|
selectedMode,
|
|
disabled,
|
|
onSelect,
|
|
}: PermissionsDropdownProps): ReactElement {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const anchorRef = useRef<View>(null);
|
|
|
|
const hasOptions = modeOptions.length > 0;
|
|
const selectedModeLabel = hasOptions
|
|
? (modeOptions.find((mode) => mode.id === selectedMode)?.label ??
|
|
modeOptions[0]?.label ??
|
|
"Default")
|
|
: "Automatic";
|
|
|
|
const options = useMemo(
|
|
() =>
|
|
modeOptions.map((mode) => ({
|
|
id: mode.id,
|
|
label: mode.label,
|
|
description: mode.description,
|
|
})),
|
|
[modeOptions],
|
|
);
|
|
|
|
const handleOpen = useCallback(() => {
|
|
if (hasOptions) {
|
|
setIsOpen(true);
|
|
}
|
|
}, [hasOptions]);
|
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
|
|
|
return (
|
|
<>
|
|
<SelectField
|
|
label="PERMISSIONS"
|
|
value={selectedModeLabel}
|
|
placeholder={hasOptions ? "Select permissions" : "Automatic"}
|
|
onPress={handleOpen}
|
|
disabled={disabled || !hasOptions}
|
|
helperText={
|
|
hasOptions ? undefined : "This assistant does not expose selectable permissions."
|
|
}
|
|
controlRef={anchorRef}
|
|
/>
|
|
{hasOptions ? (
|
|
<Combobox
|
|
options={options}
|
|
value={selectedMode}
|
|
onSelect={onSelect}
|
|
title="Permissions"
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface ModelDropdownProps {
|
|
models: AgentModelDefinition[];
|
|
selectedModel: string;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
onSelect: (modelId: string) => void;
|
|
onClear: () => void;
|
|
onRefresh: () => void;
|
|
}
|
|
|
|
export function ModelDropdown({
|
|
models,
|
|
selectedModel,
|
|
isLoading,
|
|
error,
|
|
onSelect,
|
|
onClear,
|
|
onRefresh,
|
|
}: ModelDropdownProps): ReactElement {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const anchorRef = useRef<View>(null);
|
|
|
|
const selectedLabel =
|
|
models.find((model) => model.id === selectedModel)?.label ?? selectedModel ?? "Select model";
|
|
const placeholder = isLoading && models.length === 0 ? "Loading..." : "Select model";
|
|
const helperText = error
|
|
? undefined
|
|
: isLoading
|
|
? "Fetching available models..."
|
|
: models.length === 0
|
|
? "This assistant did not expose selectable models."
|
|
: undefined;
|
|
|
|
const options = useMemo(() => {
|
|
return models.map((model) => ({
|
|
id: model.id,
|
|
label: model.label,
|
|
description: model.description,
|
|
}));
|
|
}, [models]);
|
|
|
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
|
const handleSelect = useCallback(
|
|
(id: string) => {
|
|
onSelect(id);
|
|
},
|
|
[onSelect],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<SelectField
|
|
label="MODEL"
|
|
value={selectedLabel}
|
|
placeholder={placeholder}
|
|
onPress={handleOpen}
|
|
disabled={false}
|
|
errorMessage={error ?? undefined}
|
|
helperText={helperText}
|
|
controlRef={anchorRef}
|
|
/>
|
|
<Combobox
|
|
options={options}
|
|
value={selectedModel}
|
|
onSelect={handleSelect}
|
|
title="Model"
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface WorkingDirectoryDropdownProps {
|
|
workingDir: string;
|
|
errorMessage: string;
|
|
disabled: boolean;
|
|
suggestedPaths: string[];
|
|
onSelectPath: (value: string) => void;
|
|
}
|
|
|
|
export function WorkingDirectoryDropdown({
|
|
workingDir,
|
|
errorMessage,
|
|
disabled,
|
|
suggestedPaths,
|
|
onSelectPath,
|
|
}: WorkingDirectoryDropdownProps): ReactElement {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const anchorRef = useRef<View>(null);
|
|
|
|
const options = useMemo(
|
|
() => suggestedPaths.map((path) => ({ id: path, label: path })),
|
|
[suggestedPaths],
|
|
);
|
|
|
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
|
|
|
const emptyText = "No agent directories match your search.";
|
|
|
|
return (
|
|
<>
|
|
<SelectField
|
|
label="WORKING DIRECTORY"
|
|
value={workingDir}
|
|
placeholder="Choose a working directory"
|
|
onPress={handleOpen}
|
|
disabled={disabled}
|
|
errorMessage={errorMessage || undefined}
|
|
valueEllipsizeMode="middle"
|
|
controlRef={anchorRef}
|
|
testID="working-directory-select"
|
|
/>
|
|
<Combobox
|
|
options={options}
|
|
value={workingDir}
|
|
onSelect={onSelectPath}
|
|
searchPlaceholder="Search directories..."
|
|
emptyText={emptyText}
|
|
allowCustomValue
|
|
customValuePrefix=""
|
|
customValueKind="directory"
|
|
optionsPosition="above-search"
|
|
title="Working directory"
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface ToggleRowProps {
|
|
label: string;
|
|
description?: string;
|
|
value: boolean;
|
|
onToggle: (value: boolean) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function ToggleRow({
|
|
label,
|
|
description,
|
|
value,
|
|
onToggle,
|
|
disabled,
|
|
}: ToggleRowProps): ReactElement {
|
|
return (
|
|
<Pressable
|
|
onPress={() => {
|
|
if (!disabled) {
|
|
onToggle(!value);
|
|
}
|
|
}}
|
|
style={[styles.toggleRow, disabled && styles.toggleRowDisabled]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.checkbox,
|
|
value && styles.checkboxChecked,
|
|
disabled && styles.checkboxDisabled,
|
|
]}
|
|
>
|
|
{value ? <View style={styles.checkboxDot} /> : null}
|
|
</View>
|
|
<View style={styles.toggleTextContainer}>
|
|
<Text style={styles.toggleLabel}>{label}</Text>
|
|
{description ? <Text style={styles.helperText}>{description}</Text> : null}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
export interface GitOptionsSectionProps {
|
|
worktreeMode: "none" | "create" | "attach";
|
|
onWorktreeModeChange: (value: "none" | "create" | "attach") => void;
|
|
worktreeSlug: string;
|
|
currentBranch: string | null;
|
|
baseBranch: string;
|
|
onBaseBranchChange: (value: string) => void;
|
|
status: "idle" | "loading" | "ready" | "error";
|
|
repoError: string | null;
|
|
gitValidationError: string | null;
|
|
baseBranchError: string | null;
|
|
worktreeOptions: Array<{ path: string; label: string }>;
|
|
selectedWorktreePath: string;
|
|
worktreeOptionsStatus: "idle" | "loading" | "ready" | "error";
|
|
worktreeOptionsError: string | null;
|
|
attachWorktreeError: string | null;
|
|
onSelectWorktreePath: (path: string) => void;
|
|
}
|
|
|
|
export function GitOptionsSection({
|
|
worktreeMode,
|
|
onWorktreeModeChange,
|
|
worktreeSlug,
|
|
currentBranch,
|
|
baseBranch,
|
|
onBaseBranchChange,
|
|
status,
|
|
repoError,
|
|
gitValidationError,
|
|
baseBranchError,
|
|
worktreeOptions,
|
|
selectedWorktreePath,
|
|
worktreeOptionsStatus,
|
|
worktreeOptionsError,
|
|
attachWorktreeError,
|
|
onSelectWorktreePath,
|
|
}: GitOptionsSectionProps): ReactElement {
|
|
const { theme } = useUnistyles();
|
|
|
|
const isLoading = status === "loading";
|
|
const isCreateMode = worktreeMode === "create";
|
|
const isAttachMode = worktreeMode === "attach";
|
|
const [isEditingBranch, setIsEditingBranch] = useState(false);
|
|
const [editedBranch, setEditedBranch] = useState(baseBranch);
|
|
const inputRef = useRef<TextInput>(null);
|
|
const [isWorktreeSheetOpen, setIsWorktreeSheetOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setEditedBranch(baseBranch);
|
|
}, [baseBranch]);
|
|
|
|
useEffect(() => {
|
|
if (isEditingBranch) {
|
|
inputRef.current?.focus();
|
|
}
|
|
}, [isEditingBranch]);
|
|
|
|
const handleStartEdit = useCallback(() => {
|
|
setEditedBranch(baseBranch);
|
|
setIsEditingBranch(true);
|
|
}, [baseBranch]);
|
|
|
|
const handleConfirmEdit = useCallback(() => {
|
|
const trimmed = editedBranch.trim();
|
|
if (trimmed) {
|
|
onBaseBranchChange(trimmed);
|
|
}
|
|
setIsEditingBranch(false);
|
|
}, [editedBranch, onBaseBranchChange]);
|
|
|
|
const handleCancelEdit = useCallback(() => {
|
|
setEditedBranch(baseBranch);
|
|
setIsEditingBranch(false);
|
|
}, [baseBranch]);
|
|
|
|
const displayBranch = baseBranch || currentBranch || "HEAD";
|
|
const selectedWorktreeLabel =
|
|
worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? "";
|
|
const worktreeHelperText =
|
|
worktreeOptionsStatus === "loading"
|
|
? "Loading worktrees..."
|
|
: worktreeOptions.length === 0
|
|
? "No worktrees found"
|
|
: null;
|
|
|
|
return (
|
|
<View style={styles.gitOptionsContainer}>
|
|
<Pressable
|
|
testID="worktree-create-toggle"
|
|
onPress={() => onWorktreeModeChange(isCreateMode ? "none" : "create")}
|
|
disabled={isLoading}
|
|
style={[styles.worktreeToggle, isLoading && styles.worktreeToggleDisabled]}
|
|
>
|
|
<View style={[styles.checkbox, isCreateMode && styles.checkboxChecked]}>
|
|
{isCreateMode ? <View style={styles.checkboxDot} /> : null}
|
|
</View>
|
|
<View style={styles.worktreeToggleContent}>
|
|
<Text style={styles.worktreeToggleLabel}>Create worktree</Text>
|
|
<Text style={styles.worktreeToggleDescription}>
|
|
{isLoading
|
|
? "Inspecting repository…"
|
|
: isCreateMode
|
|
? `Will create: ${worktreeSlug || "preparing…"}`
|
|
: currentBranch
|
|
? `Run isolated from ${currentBranch}`
|
|
: "Run in an isolated directory"}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
|
|
<Pressable
|
|
testID="worktree-attach-toggle"
|
|
onPress={() => onWorktreeModeChange(isAttachMode ? "none" : "attach")}
|
|
disabled={isLoading}
|
|
style={[styles.worktreeToggle, isLoading && styles.worktreeToggleDisabled]}
|
|
>
|
|
<View style={[styles.checkbox, isAttachMode && styles.checkboxChecked]}>
|
|
{isAttachMode ? <View style={styles.checkboxDot} /> : null}
|
|
</View>
|
|
<View style={styles.worktreeToggleContent}>
|
|
<Text style={styles.worktreeToggleLabel}>Attach to existing worktree</Text>
|
|
<Text style={styles.worktreeToggleDescription}>
|
|
{isLoading ? "Inspecting repository…" : "Pick a Paseo worktree by branch"}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
|
|
{isAttachMode ? (
|
|
<>
|
|
<SelectField
|
|
label="Worktree"
|
|
value={selectedWorktreeLabel}
|
|
placeholder="Select a worktree"
|
|
onPress={() => setIsWorktreeSheetOpen(true)}
|
|
disabled={isLoading || worktreeOptionsStatus === "loading"}
|
|
helperText={worktreeHelperText}
|
|
errorMessage={attachWorktreeError || worktreeOptionsError}
|
|
testID="worktree-attach-picker"
|
|
/>
|
|
<DropdownSheet
|
|
title="Select worktree"
|
|
visible={isWorktreeSheetOpen}
|
|
onClose={() => setIsWorktreeSheetOpen(false)}
|
|
>
|
|
{worktreeOptions.map((option, index) => (
|
|
<SelectOption
|
|
key={option.path}
|
|
label={option.label}
|
|
selected={option.path === selectedWorktreePath}
|
|
testID={`worktree-attach-option-${index}`}
|
|
onPress={() => {
|
|
onSelectWorktreePath(option.path);
|
|
setIsWorktreeSheetOpen(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</DropdownSheet>
|
|
</>
|
|
) : null}
|
|
|
|
{isCreateMode ? (
|
|
<View style={styles.baseBranchRow}>
|
|
<Text style={styles.baseBranchLabel}>Base branch:</Text>
|
|
{isEditingBranch ? (
|
|
<View style={styles.baseBranchEditRow}>
|
|
<TextInput
|
|
ref={inputRef}
|
|
style={styles.baseBranchInput}
|
|
value={editedBranch}
|
|
onChangeText={setEditedBranch}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
placeholder="branch name"
|
|
placeholderTextColor={theme.colors.foregroundMuted}
|
|
onSubmitEditing={handleConfirmEdit}
|
|
/>
|
|
<Pressable
|
|
onPress={handleConfirmEdit}
|
|
hitSlop={8}
|
|
style={styles.baseBranchIconButton}
|
|
>
|
|
<Check size={theme.iconSize.md} color={theme.colors.palette.green[500]} />
|
|
</Pressable>
|
|
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
|
<X size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
</View>
|
|
) : (
|
|
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
|
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
|
<Pencil size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
) : null}
|
|
|
|
{baseBranchError ? <Text style={styles.errorText}>{baseBranchError}</Text> : null}
|
|
|
|
{repoError ? <Text style={styles.errorText}>{repoError}</Text> : null}
|
|
|
|
{gitValidationError ? <Text style={styles.errorText}>{gitValidationError}</Text> : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
formSection: {
|
|
gap: theme.spacing[3],
|
|
},
|
|
label: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
dropdownControl: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[3],
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.lg,
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
dropdownControlDisabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
dropdownValue: {
|
|
flex: 1,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
dropdownPlaceholder: {
|
|
flex: 1,
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
bottomSheetHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingHorizontal: theme.spacing[6],
|
|
paddingBottom: theme.spacing[2],
|
|
},
|
|
dropdownSheetOverlay: {
|
|
flex: 1,
|
|
justifyContent: "flex-end",
|
|
},
|
|
dropdownSheetBackdrop: {
|
|
position: "absolute",
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
backgroundColor: theme.colors.palette.gray[900],
|
|
opacity: 0.45,
|
|
},
|
|
dropdownSheetContainer: {
|
|
backgroundColor: theme.colors.surface2,
|
|
borderTopLeftRadius: theme.borderRadius["2xl"],
|
|
borderTopRightRadius: theme.borderRadius["2xl"],
|
|
paddingTop: theme.spacing[4],
|
|
paddingHorizontal: theme.spacing[6],
|
|
paddingBottom: theme.spacing[6] + theme.spacing[2],
|
|
maxHeight: 560,
|
|
width: "100%",
|
|
},
|
|
dropdownSheetHandle: {
|
|
width: 56,
|
|
height: 4,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.border,
|
|
alignSelf: "center",
|
|
marginBottom: theme.spacing[3],
|
|
},
|
|
dropdownSheetTitle: {
|
|
fontSize: theme.fontSize.lg,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
textAlign: "center",
|
|
},
|
|
dropdownSheetScrollContent: {
|
|
paddingBottom: theme.spacing[8],
|
|
paddingHorizontal: theme.spacing[1],
|
|
},
|
|
dropdownSheetList: {
|
|
marginTop: theme.spacing[3],
|
|
},
|
|
dropdownSheetOption: {
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
borderRadius: theme.borderRadius.lg,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
backgroundColor: theme.colors.surface0,
|
|
marginBottom: theme.spacing[2],
|
|
},
|
|
dropdownSheetOptionSelected: {
|
|
borderColor: theme.colors.palette.blue[400],
|
|
backgroundColor: "rgba(59, 130, 246, 0.18)",
|
|
},
|
|
dropdownSheetOptionLabel: {
|
|
color: theme.colors.foreground,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
dropdownSheetOptionDescription: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
marginTop: theme.spacing[1],
|
|
},
|
|
dropdownSheetLoading: {
|
|
alignItems: "center",
|
|
paddingVertical: theme.spacing[4],
|
|
},
|
|
errorText: {
|
|
color: theme.colors.palette.red[500],
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
warningText: {
|
|
color: theme.colors.palette.orange[500],
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
helperText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
selectorColumn: {
|
|
flex: 1,
|
|
gap: theme.spacing[3],
|
|
},
|
|
selectorColumnFull: {
|
|
width: "100%",
|
|
},
|
|
toggleRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[3],
|
|
paddingVertical: theme.spacing[2],
|
|
},
|
|
toggleRowDisabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
checkbox: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: theme.borderRadius.sm,
|
|
borderWidth: theme.borderWidth[2],
|
|
borderColor: theme.colors.border,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
checkboxChecked: {
|
|
borderColor: theme.colors.palette.blue[500],
|
|
backgroundColor: theme.colors.palette.blue[500],
|
|
},
|
|
checkboxDisabled: {
|
|
borderColor: theme.colors.border,
|
|
},
|
|
checkboxDot: {
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.palette.white,
|
|
},
|
|
toggleTextContainer: {
|
|
flex: 1,
|
|
gap: theme.spacing[1],
|
|
},
|
|
toggleLabel: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
input: {
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.lg,
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
dropdownLoading: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
selectFieldContainer: {
|
|
gap: theme.spacing[2],
|
|
},
|
|
selectFieldControl: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.lg,
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
selectFieldControlDisabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
selectFieldContent: {
|
|
flex: 1,
|
|
gap: theme.spacing[1],
|
|
},
|
|
selectFieldLabel: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
fontWeight: theme.fontWeight.medium,
|
|
textTransform: "uppercase",
|
|
letterSpacing: 0.5,
|
|
},
|
|
selectFieldValue: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
selectFieldPlaceholder: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
gitOptionsContainer: {
|
|
gap: theme.spacing[3],
|
|
},
|
|
worktreeToggle: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[3],
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.lg,
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
worktreeToggleDisabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
worktreeToggleContent: {
|
|
flex: 1,
|
|
gap: theme.spacing[1],
|
|
},
|
|
worktreeToggleLabel: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
worktreeToggleDescription: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
baseBranchRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
baseBranchLabel: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
baseBranchValueRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
baseBranchValue: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.medium,
|
|
},
|
|
baseBranchEditRow: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
baseBranchInput: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.md,
|
|
paddingVertical: theme.spacing[1],
|
|
paddingHorizontal: theme.spacing[2],
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
baseBranchIconButton: {
|
|
padding: theme.spacing[1],
|
|
},
|
|
desktopDropdownOverlay: {
|
|
flex: 1,
|
|
},
|
|
desktopDropdownBackdrop: {
|
|
position: "absolute",
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
},
|
|
desktopDropdownContainer: {
|
|
backgroundColor: theme.colors.surface0,
|
|
borderRadius: theme.borderRadius.lg,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.borderAccent,
|
|
...theme.shadow.md,
|
|
maxHeight: 400,
|
|
overflow: "hidden",
|
|
},
|
|
desktopDropdownScroll: {
|
|
maxHeight: 400,
|
|
},
|
|
desktopDropdownScrollContent: {
|
|
paddingVertical: theme.spacing[1],
|
|
},
|
|
agentConfigRow: {
|
|
flexDirection: {
|
|
xs: "column",
|
|
md: "row",
|
|
},
|
|
gap: theme.spacing[2],
|
|
},
|
|
agentConfigColumn: {
|
|
flex: 1,
|
|
},
|
|
compactSelectControl: {
|
|
backgroundColor: theme.colors.surface0,
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.lg,
|
|
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.sm,
|
|
fontWeight: theme.fontWeight.medium,
|
|
},
|
|
compactSelectValue: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
compactSelectPlaceholder: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
}));
|