* feat(codex): plumb auto-review approvals reviewer * feat(codex): add Auto-review mode visuals * test(codex): cover Auto-review mode plumbing * test(codex): prove Auto-review approval routing
2076 lines
64 KiB
TypeScript
2076 lines
64 KiB
TypeScript
import {
|
|
memo,
|
|
useCallback,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type ReactElement,
|
|
type RefObject,
|
|
} from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
Pressable,
|
|
Keyboard,
|
|
type PressableStateCallbackType,
|
|
type StyleProp,
|
|
type ViewStyle,
|
|
} from "react-native";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import { useShallow } from "zustand/shallow";
|
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
|
import {
|
|
Brain,
|
|
ChevronDown,
|
|
ListTodo,
|
|
Settings2,
|
|
ShieldAlert,
|
|
ShieldCheck,
|
|
ShieldOff,
|
|
ShieldQuestionMark,
|
|
Zap,
|
|
} from "lucide-react-native";
|
|
import { getProviderIcon } from "@/components/provider-icons";
|
|
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
|
import { resolveProviderDefinition } from "@/utils/provider-definitions";
|
|
import {
|
|
buildFavoriteModelKey,
|
|
mergeProviderPreferences,
|
|
toggleFavoriteModel,
|
|
useFormPreferences,
|
|
} from "@/hooks/use-form-preferences";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
import type {
|
|
AgentFeature,
|
|
AgentMode,
|
|
AgentModelDefinition,
|
|
AgentProvider,
|
|
} from "@server/server/agent/agent-sdk-types";
|
|
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
|
import { getModeVisuals, type AgentModeColorTier } from "@server/server/agent/provider-manifest";
|
|
import {
|
|
getFeatureHighlightColor,
|
|
getFeatureTooltip,
|
|
getStatusSelectorHint,
|
|
resolveAgentModelSelection,
|
|
} from "@/components/agent-status-bar.utils";
|
|
import { isWeb as platformIsWeb } from "@/constants/platform";
|
|
import { useToast } from "@/contexts/toast-context";
|
|
import { toErrorMessage } from "@/utils/error-messages";
|
|
|
|
interface StatusOption {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
type StatusSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
|
|
|
|
interface ControlledAgentStatusBarProps {
|
|
provider: string;
|
|
providerOptions?: StatusOption[];
|
|
selectedProviderId?: string;
|
|
onSelectProvider?: (providerId: string) => void;
|
|
modeOptions?: StatusOption[];
|
|
selectedModeId?: string;
|
|
onSelectMode?: (modeId: string) => void;
|
|
modelOptions?: StatusOption[];
|
|
selectedModelId?: string;
|
|
onSelectModel?: (modelId: string) => void;
|
|
onSelectProviderAndModel?: (provider: string, modelId: string) => void;
|
|
thinkingOptions?: StatusOption[];
|
|
selectedThinkingOptionId?: string;
|
|
onSelectThinkingOption?: (thinkingOptionId: string) => void;
|
|
disabled?: boolean;
|
|
isModelLoading?: boolean;
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
allProviderModels?: Map<string, AgentModelDefinition[]>;
|
|
canSelectModelProvider?: (providerId: string) => boolean;
|
|
favoriteKeys?: Set<string>;
|
|
onToggleFavoriteModel?: (provider: string, modelId: string) => void;
|
|
features?: AgentFeature[];
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
onDropdownClose?: () => void;
|
|
onModelSelectorOpen?: () => void;
|
|
}
|
|
|
|
export interface DraftAgentStatusBarProps {
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
selectedProvider: AgentProvider | null;
|
|
onSelectProvider: (provider: AgentProvider) => void;
|
|
modeOptions: AgentMode[];
|
|
selectedMode: string;
|
|
onSelectMode: (modeId: string) => void;
|
|
models: AgentModelDefinition[];
|
|
selectedModel: string;
|
|
onSelectModel: (modelId: string) => void;
|
|
isModelLoading: boolean;
|
|
allProviderModels: Map<string, AgentModelDefinition[]>;
|
|
isAllModelsLoading: boolean;
|
|
onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void;
|
|
thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
|
|
selectedThinkingOptionId: string;
|
|
onSelectThinkingOption: (thinkingOptionId: string) => void;
|
|
features?: AgentFeature[];
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
onDropdownClose?: () => void;
|
|
onModelSelectorOpen?: () => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
interface AgentStatusBarProps {
|
|
agentId: string;
|
|
serverId: string;
|
|
onDropdownClose?: () => void;
|
|
}
|
|
|
|
function findOptionLabel(
|
|
options: StatusOption[] | undefined,
|
|
selectedId: string | undefined,
|
|
fallback: string,
|
|
) {
|
|
if (!options || options.length === 0) {
|
|
return fallback;
|
|
}
|
|
const selected = options.find((option) => option.id === selectedId);
|
|
return selected?.label ?? fallback;
|
|
}
|
|
|
|
const FEATURE_ICONS: Record<string, typeof Zap> = {
|
|
"list-todo": ListTodo,
|
|
zap: Zap,
|
|
};
|
|
|
|
function getFeatureIcon(icon?: string) {
|
|
return (icon && FEATURE_ICONS[icon]) || Settings2;
|
|
}
|
|
|
|
function getFeatureIconColor(
|
|
featureId: string,
|
|
enabled: boolean,
|
|
palette: {
|
|
blue: { 400: string };
|
|
yellow: { 400: string };
|
|
},
|
|
foregroundMuted: string,
|
|
): string {
|
|
if (!enabled) {
|
|
return foregroundMuted;
|
|
}
|
|
|
|
switch (getFeatureHighlightColor(featureId)) {
|
|
case "blue":
|
|
return palette.blue[400];
|
|
case "yellow":
|
|
return palette.yellow[400];
|
|
default:
|
|
return foregroundMuted;
|
|
}
|
|
}
|
|
|
|
const MODE_ICONS = {
|
|
ShieldCheck,
|
|
ShieldAlert,
|
|
ShieldOff,
|
|
ShieldQuestionMark,
|
|
} as const;
|
|
|
|
function alwaysTrue() {
|
|
return true;
|
|
}
|
|
|
|
function resolveDisplayModel(
|
|
isModelLoading: boolean,
|
|
modelOptions: StatusOption[] | undefined,
|
|
selectedModelId: string | undefined,
|
|
) {
|
|
if (isModelLoading && (!modelOptions || modelOptions.length === 0)) {
|
|
return "Loading models...";
|
|
}
|
|
return findOptionLabel(modelOptions, selectedModelId, "Select model");
|
|
}
|
|
|
|
function resolveHasAnyControl({
|
|
providerOptions,
|
|
modeOptions,
|
|
canSelectModel,
|
|
thinkingOptions,
|
|
features,
|
|
}: {
|
|
providerOptions: StatusOption[] | undefined;
|
|
modeOptions: StatusOption[] | undefined;
|
|
canSelectModel: boolean;
|
|
thinkingOptions: StatusOption[] | undefined;
|
|
features: AgentFeature[] | undefined;
|
|
}) {
|
|
return (
|
|
Boolean(providerOptions?.length) ||
|
|
Boolean(modeOptions?.length) ||
|
|
canSelectModel ||
|
|
Boolean(thinkingOptions?.length) ||
|
|
Boolean(features?.length)
|
|
);
|
|
}
|
|
|
|
function toComboboxOptions(options: StatusOption[] | undefined): ComboboxOption[] {
|
|
return (options ?? []).map((o) => ({ id: o.id, label: o.label }));
|
|
}
|
|
|
|
function buildFallbackAllProviderModels(
|
|
provider: string,
|
|
modelOptions: StatusOption[] | undefined,
|
|
): Map<string, AgentModelDefinition[]> {
|
|
const map = new Map<string, AgentModelDefinition[]>();
|
|
if (!modelOptions || modelOptions.length === 0) {
|
|
return map;
|
|
}
|
|
map.set(
|
|
provider,
|
|
modelOptions.map((option) => ({
|
|
provider: provider,
|
|
id: option.id,
|
|
label: option.label,
|
|
})),
|
|
);
|
|
return map;
|
|
}
|
|
|
|
function makeBadgePressableStyle(
|
|
baseStyle: StyleProp<ViewStyle>,
|
|
disabledStyle: StyleProp<ViewStyle>,
|
|
disabled: boolean,
|
|
isOpen: boolean,
|
|
) {
|
|
return ({ pressed, hovered }: PressableStateCallbackType) => [
|
|
baseStyle,
|
|
hovered && styles.modeBadgeHovered,
|
|
(pressed || isOpen) && styles.modeBadgePressed,
|
|
disabled && disabledStyle,
|
|
];
|
|
}
|
|
|
|
function makeSheetPressableStyle(isDisabled: boolean) {
|
|
return ({ pressed }: PressableStateCallbackType) => [
|
|
styles.sheetSelect,
|
|
pressed && styles.sheetSelectPressed,
|
|
isDisabled && styles.disabledSheetSelect,
|
|
];
|
|
}
|
|
|
|
function makePrefsButtonStyle({ pressed }: PressableStateCallbackType) {
|
|
return [styles.prefsButton, pressed && styles.prefsButtonPressed];
|
|
}
|
|
|
|
function pickSheetModel({
|
|
nextProviderId,
|
|
modelId,
|
|
currentProvider,
|
|
onSelectProviderAndModel,
|
|
onSelectProvider,
|
|
onSelectModel,
|
|
}: {
|
|
nextProviderId: string;
|
|
modelId: string;
|
|
currentProvider: string;
|
|
onSelectProviderAndModel?: (provider: string, modelId: string) => void;
|
|
onSelectProvider?: (providerId: string) => void;
|
|
onSelectModel?: (modelId: string) => void;
|
|
}) {
|
|
if (onSelectProviderAndModel) {
|
|
onSelectProviderAndModel(nextProviderId, modelId);
|
|
return;
|
|
}
|
|
if (nextProviderId !== currentProvider) {
|
|
onSelectProvider?.(nextProviderId);
|
|
}
|
|
onSelectModel?.(modelId);
|
|
}
|
|
|
|
function pickDesktopModel({
|
|
nextProviderId,
|
|
modelId,
|
|
currentProvider,
|
|
onSelectModel,
|
|
}: {
|
|
nextProviderId: string;
|
|
modelId: string;
|
|
currentProvider: string;
|
|
onSelectModel?: (modelId: string) => void;
|
|
}) {
|
|
if (nextProviderId === currentProvider) {
|
|
onSelectModel?.(modelId);
|
|
}
|
|
}
|
|
|
|
function resolveModeVisualsForProvider(
|
|
provider: string,
|
|
selectedModeId: string | undefined,
|
|
providerDefinitions: AgentProviderDefinition[],
|
|
palette: Parameters<typeof getModeIconColor>[1],
|
|
) {
|
|
const modeVisuals = selectedModeId
|
|
? getModeVisuals(provider, selectedModeId, providerDefinitions)
|
|
: undefined;
|
|
const icon = modeVisuals?.icon ? MODE_ICONS[modeVisuals.icon] : null;
|
|
const color = getModeIconColor(modeVisuals?.colorTier, palette);
|
|
return { icon, color };
|
|
}
|
|
|
|
function resolveProviderIcon(provider: string) {
|
|
if (provider.trim().length === 0) {
|
|
return null;
|
|
}
|
|
return getProviderIcon(provider);
|
|
}
|
|
|
|
type AgentStatusBarSlice = {
|
|
provider: string;
|
|
cwd: string | null;
|
|
currentModeId: string | null | undefined;
|
|
runtimeModelId: string | null;
|
|
model: string | null | undefined;
|
|
features: AgentFeature[] | undefined;
|
|
thinkingOptionId: string | null | undefined;
|
|
lastUsage: unknown;
|
|
} | null;
|
|
|
|
function selectAgentStatusBarSlice(
|
|
state: ReturnType<typeof useSessionStore.getState>,
|
|
serverId: string,
|
|
agentId: string,
|
|
): AgentStatusBarSlice {
|
|
const currentAgent = state.sessions[serverId]?.agents?.get(agentId) ?? null;
|
|
if (!currentAgent) {
|
|
return null;
|
|
}
|
|
return {
|
|
provider: currentAgent.provider,
|
|
cwd: currentAgent.cwd,
|
|
currentModeId: currentAgent.currentModeId,
|
|
runtimeModelId: currentAgent.runtimeInfo?.model ?? null,
|
|
model: currentAgent.model,
|
|
features: currentAgent.features,
|
|
thinkingOptionId: currentAgent.thinkingOptionId,
|
|
lastUsage: currentAgent.lastUsage,
|
|
};
|
|
}
|
|
|
|
function resolveSnapshotSelectedEntry(
|
|
snapshotEntries: ReturnType<typeof useProvidersSnapshot>["entries"],
|
|
agentProvider: string | undefined,
|
|
) {
|
|
if (!snapshotEntries || !agentProvider) {
|
|
return null;
|
|
}
|
|
return snapshotEntries.find((e) => e.provider === agentProvider) ?? null;
|
|
}
|
|
|
|
function buildAgentProviderDefinitions(
|
|
agentProvider: string | undefined,
|
|
snapshotEntries: ReturnType<typeof useProvidersSnapshot>["entries"],
|
|
): AgentProviderDefinition[] {
|
|
const definition = agentProvider
|
|
? resolveProviderDefinition(agentProvider, snapshotEntries)
|
|
: undefined;
|
|
return definition ? [definition] : [];
|
|
}
|
|
|
|
function buildAgentProviderModels(
|
|
agentProvider: string | undefined,
|
|
models: AgentModelDefinition[] | null,
|
|
): Map<string, AgentModelDefinition[]> {
|
|
const map = new Map<string, AgentModelDefinition[]>();
|
|
if (agentProvider && models) {
|
|
map.set(agentProvider, models);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function compareAvailableModes(a: AgentMode[], b: AgentMode[]): boolean {
|
|
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
}
|
|
|
|
function resolveAgentDisplayMode(
|
|
availableModes: AgentMode[],
|
|
currentModeId: string | null | undefined,
|
|
): string {
|
|
const found = availableModes.find((mode) => mode.id === currentModeId);
|
|
return found?.label || currentModeId || "default";
|
|
}
|
|
|
|
function buildOpenChangeHandler(
|
|
selector: StatusSelector,
|
|
setOpenSelector: (next: StatusSelector | null) => void,
|
|
onDropdownClose?: () => void,
|
|
) {
|
|
return (nextOpen: boolean) => {
|
|
setOpenSelector(nextOpen ? selector : null);
|
|
if (!nextOpen) {
|
|
onDropdownClose?.();
|
|
}
|
|
};
|
|
}
|
|
|
|
function SheetModelTriggerView({
|
|
selectedModelLabel,
|
|
style,
|
|
ProviderIcon,
|
|
}: {
|
|
selectedModelLabel: string;
|
|
style: StyleProp<ViewStyle>;
|
|
ProviderIcon: ReturnType<typeof getProviderIcon> | null;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
return (
|
|
<View style={style} pointerEvents="none" testID="agent-preferences-model">
|
|
{ProviderIcon ? (
|
|
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
) : null}
|
|
<Text style={styles.sheetSelectText}>{selectedModelLabel}</Text>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function getModeIconColor(
|
|
colorTier: AgentModeColorTier | undefined,
|
|
palette: {
|
|
blue: { 500: string };
|
|
green: { 500: string };
|
|
red: { 500: string };
|
|
purple: { 500: string };
|
|
},
|
|
): string {
|
|
switch (colorTier) {
|
|
case "safe":
|
|
return palette.green[500];
|
|
case "moderate":
|
|
return palette.blue[500];
|
|
case "dangerous":
|
|
return palette.red[500];
|
|
case "planning":
|
|
return palette.purple[500];
|
|
default:
|
|
return palette.blue[500];
|
|
}
|
|
}
|
|
|
|
function ControlledStatusBar({
|
|
provider,
|
|
providerOptions,
|
|
selectedProviderId,
|
|
onSelectProvider,
|
|
modeOptions,
|
|
selectedModeId,
|
|
onSelectMode,
|
|
modelOptions,
|
|
selectedModelId,
|
|
onSelectModel,
|
|
onSelectProviderAndModel,
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
onSelectThinkingOption,
|
|
disabled = false,
|
|
isModelLoading = false,
|
|
providerDefinitions,
|
|
allProviderModels,
|
|
canSelectModelProvider,
|
|
favoriteKeys = new Set<string>(),
|
|
onToggleFavoriteModel,
|
|
features,
|
|
onSetFeature,
|
|
onDropdownClose,
|
|
onModelSelectorOpen,
|
|
}: ControlledAgentStatusBarProps) {
|
|
const { theme } = useUnistyles();
|
|
const [prefsOpen, setPrefsOpen] = useState(false);
|
|
const [openSelector, setOpenSelector] = useState<StatusSelector | null>(null);
|
|
|
|
const providerAnchorRef = useRef<View>(null);
|
|
const modeAnchorRef = useRef<View>(null);
|
|
const _modelAnchorRef = useRef<View>(null);
|
|
const thinkingAnchorRef = useRef<View>(null);
|
|
|
|
const canSelectProvider = Boolean(
|
|
onSelectProvider && providerOptions && providerOptions.length > 0,
|
|
);
|
|
const canSelectMode = Boolean(onSelectMode && modeOptions && modeOptions.length > 0);
|
|
const canSelectModel = Boolean(onSelectModel);
|
|
const canSelectThinking = Boolean(
|
|
onSelectThinkingOption && thinkingOptions && thinkingOptions.length > 0,
|
|
);
|
|
|
|
const displayProvider = findOptionLabel(providerOptions, selectedProviderId, "Provider");
|
|
const displayMode = findOptionLabel(modeOptions, selectedModeId, "Default");
|
|
const displayModel = resolveDisplayModel(isModelLoading, modelOptions, selectedModelId);
|
|
const displayThinking = findOptionLabel(
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
thinkingOptions?.[0]?.label ?? "Unknown",
|
|
);
|
|
|
|
const { icon: ModeIconComponent, color: modeIconColor } = resolveModeVisualsForProvider(
|
|
provider,
|
|
selectedModeId,
|
|
providerDefinitions,
|
|
theme.colors.palette,
|
|
);
|
|
const ProviderIcon = resolveProviderIcon(provider);
|
|
|
|
const hasAnyControl = resolveHasAnyControl({
|
|
providerOptions,
|
|
modeOptions,
|
|
canSelectModel,
|
|
thinkingOptions,
|
|
features,
|
|
});
|
|
|
|
const modelDisabled = disabled;
|
|
|
|
const comboboxProviderOptions = useMemo<ComboboxOption[]>(
|
|
() => toComboboxOptions(providerOptions),
|
|
[providerOptions],
|
|
);
|
|
const comboboxModeOptions = useMemo<ComboboxOption[]>(
|
|
() => toComboboxOptions(modeOptions),
|
|
[modeOptions],
|
|
);
|
|
const fallbackAllProviderModels = useMemo(
|
|
() => buildFallbackAllProviderModels(provider, modelOptions),
|
|
[modelOptions, provider],
|
|
);
|
|
const effectiveProviderDefinitions = providerDefinitions;
|
|
const effectiveAllProviderModels = allProviderModels ?? fallbackAllProviderModels;
|
|
const canSelectProviderInModelMenu = canSelectModelProvider ?? alwaysTrue;
|
|
const comboboxThinkingOptions = useMemo<ComboboxOption[]>(
|
|
() => toComboboxOptions(thinkingOptions),
|
|
[thinkingOptions],
|
|
);
|
|
|
|
const renderModeOption = useCallback(
|
|
(args: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void }) => (
|
|
<ModeComboboxOption
|
|
option={args.option}
|
|
selected={args.selected}
|
|
active={args.active}
|
|
onPress={args.onPress}
|
|
provider={provider}
|
|
providerDefinitions={providerDefinitions}
|
|
iconColor={theme.colors.foreground}
|
|
/>
|
|
),
|
|
[provider, providerDefinitions, theme.colors.foreground],
|
|
);
|
|
|
|
const handleOpenChange = useCallback(
|
|
(selector: StatusSelector) =>
|
|
buildOpenChangeHandler(selector, setOpenSelector, onDropdownClose),
|
|
[onDropdownClose],
|
|
);
|
|
|
|
const handleProviderPress = useCallback(() => {
|
|
handleOpenChange("provider")(openSelector !== "provider");
|
|
}, [handleOpenChange, openSelector]);
|
|
|
|
const handleThinkingPress = useCallback(() => {
|
|
handleOpenChange("thinking")(openSelector !== "thinking");
|
|
}, [handleOpenChange, openSelector]);
|
|
|
|
const handleModePress = useCallback(() => {
|
|
handleOpenChange("mode")(openSelector !== "mode");
|
|
}, [handleOpenChange, openSelector]);
|
|
|
|
const handleProviderOpenChange = useMemo(() => handleOpenChange("provider"), [handleOpenChange]);
|
|
const handleThinkingOpenChange = useMemo(() => handleOpenChange("thinking"), [handleOpenChange]);
|
|
const handleModeOpenChange = useMemo(() => handleOpenChange("mode"), [handleOpenChange]);
|
|
|
|
const handleProviderSelect = useCallback(
|
|
(id: string) => onSelectProvider?.(id),
|
|
[onSelectProvider],
|
|
);
|
|
const handleThinkingSelect = useCallback(
|
|
(id: string) => onSelectThinkingOption?.(id),
|
|
[onSelectThinkingOption],
|
|
);
|
|
const handleModeSelect = useCallback((id: string) => onSelectMode?.(id), [onSelectMode]);
|
|
|
|
const handleDesktopModelSelect = useCallback(
|
|
(nextProviderId: string, modelId: string) => {
|
|
pickDesktopModel({ nextProviderId, modelId, currentProvider: provider, onSelectModel });
|
|
},
|
|
[onSelectModel, provider],
|
|
);
|
|
|
|
const providerPressableStyle = useMemo(
|
|
() =>
|
|
makeBadgePressableStyle(
|
|
styles.modeBadge,
|
|
styles.disabledBadge,
|
|
disabled || !canSelectProvider,
|
|
openSelector === "provider",
|
|
),
|
|
[canSelectProvider, disabled, openSelector],
|
|
);
|
|
|
|
const thinkingPressableStyle = useMemo(
|
|
() =>
|
|
makeBadgePressableStyle(
|
|
styles.modeBadge,
|
|
styles.disabledBadge,
|
|
disabled || !canSelectThinking,
|
|
openSelector === "thinking",
|
|
),
|
|
[canSelectThinking, disabled, openSelector],
|
|
);
|
|
|
|
const modePressableStyle = useMemo(
|
|
() =>
|
|
makeBadgePressableStyle(
|
|
styles.modeIconBadge,
|
|
styles.disabledBadge,
|
|
disabled || !canSelectMode,
|
|
openSelector === "mode",
|
|
),
|
|
[canSelectMode, disabled, openSelector],
|
|
);
|
|
|
|
const handleOpenPrefs = useCallback(() => {
|
|
Keyboard.dismiss();
|
|
setPrefsOpen(true);
|
|
}, []);
|
|
|
|
const handleClosePrefs = useCallback(() => {
|
|
setPrefsOpen(false);
|
|
}, []);
|
|
|
|
const prefsButtonStyle = makePrefsButtonStyle;
|
|
|
|
const handleSheetModelSelect = useCallback(
|
|
(nextProviderId: string, modelId: string) => {
|
|
pickSheetModel({
|
|
nextProviderId,
|
|
modelId,
|
|
currentProvider: provider,
|
|
onSelectProviderAndModel,
|
|
onSelectProvider,
|
|
onSelectModel,
|
|
});
|
|
},
|
|
[onSelectModel, onSelectProvider, onSelectProviderAndModel, provider],
|
|
);
|
|
|
|
const sheetThinkingPressableStyle = useMemo(
|
|
() => makeSheetPressableStyle(disabled || !canSelectThinking),
|
|
[canSelectThinking, disabled],
|
|
);
|
|
|
|
const sheetModePressableStyle = useMemo(
|
|
() => makeSheetPressableStyle(disabled || !canSelectMode),
|
|
[canSelectMode, disabled],
|
|
);
|
|
|
|
const sheetSelectStyle = useMemo(
|
|
() => [styles.sheetSelect, modelDisabled && styles.disabledSheetSelect],
|
|
[modelDisabled],
|
|
);
|
|
const renderSheetModelTrigger = useCallback(
|
|
({ selectedModelLabel }: { selectedModelLabel: string }) => (
|
|
<SheetModelTriggerView
|
|
selectedModelLabel={selectedModelLabel}
|
|
style={sheetSelectStyle}
|
|
ProviderIcon={ProviderIcon}
|
|
/>
|
|
),
|
|
[ProviderIcon, sheetSelectStyle],
|
|
);
|
|
|
|
if (!hasAnyControl) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{platformIsWeb ? (
|
|
<DesktopStatusBarContent
|
|
provider={provider}
|
|
providerOptions={providerOptions}
|
|
selectedProviderId={selectedProviderId}
|
|
modeOptions={modeOptions}
|
|
selectedModeId={selectedModeId}
|
|
modelOptions={modelOptions}
|
|
selectedModelId={selectedModelId}
|
|
thinkingOptions={thinkingOptions}
|
|
selectedThinkingOptionId={selectedThinkingOptionId}
|
|
features={features}
|
|
onSetFeature={onSetFeature}
|
|
onToggleFavoriteModel={onToggleFavoriteModel}
|
|
onDropdownClose={onDropdownClose}
|
|
onModelSelectorOpen={onModelSelectorOpen}
|
|
providerDefinitions={providerDefinitions}
|
|
favoriteKeys={favoriteKeys}
|
|
disabled={disabled}
|
|
isModelLoading={isModelLoading}
|
|
canSelectProvider={canSelectProvider}
|
|
canSelectMode={canSelectMode}
|
|
canSelectModel={canSelectModel}
|
|
canSelectThinking={canSelectThinking}
|
|
canSelectProviderInModelMenu={canSelectProviderInModelMenu}
|
|
modelDisabled={modelDisabled}
|
|
effectiveProviderDefinitions={effectiveProviderDefinitions}
|
|
effectiveAllProviderModels={effectiveAllProviderModels}
|
|
comboboxProviderOptions={comboboxProviderOptions}
|
|
comboboxModeOptions={comboboxModeOptions}
|
|
comboboxThinkingOptions={comboboxThinkingOptions}
|
|
displayProvider={displayProvider}
|
|
displayModel={displayModel}
|
|
displayThinking={displayThinking}
|
|
ModeIconComponent={ModeIconComponent}
|
|
modeIconColor={modeIconColor}
|
|
openSelector={openSelector}
|
|
providerAnchorRef={providerAnchorRef}
|
|
thinkingAnchorRef={thinkingAnchorRef}
|
|
modeAnchorRef={modeAnchorRef}
|
|
providerPressableStyle={providerPressableStyle}
|
|
thinkingPressableStyle={thinkingPressableStyle}
|
|
modePressableStyle={modePressableStyle}
|
|
handleProviderPress={handleProviderPress}
|
|
handleThinkingPress={handleThinkingPress}
|
|
handleModePress={handleModePress}
|
|
handleProviderSelect={handleProviderSelect}
|
|
handleThinkingSelect={handleThinkingSelect}
|
|
handleModeSelect={handleModeSelect}
|
|
handleDesktopModelSelect={handleDesktopModelSelect}
|
|
handleProviderOpenChange={handleProviderOpenChange}
|
|
handleThinkingOpenChange={handleThinkingOpenChange}
|
|
handleModeOpenChange={handleModeOpenChange}
|
|
handleOpenChange={handleOpenChange}
|
|
renderModeOption={renderModeOption}
|
|
/>
|
|
) : (
|
|
<SheetStatusBarContent
|
|
provider={provider}
|
|
modeOptions={modeOptions}
|
|
selectedModeId={selectedModeId}
|
|
selectedModelId={selectedModelId}
|
|
thinkingOptions={thinkingOptions}
|
|
selectedThinkingOptionId={selectedThinkingOptionId}
|
|
features={features}
|
|
onSetFeature={onSetFeature}
|
|
onSelectMode={onSelectMode}
|
|
onSelectThinkingOption={onSelectThinkingOption}
|
|
onToggleFavoriteModel={onToggleFavoriteModel}
|
|
onDropdownClose={onDropdownClose}
|
|
onModelSelectorOpen={onModelSelectorOpen}
|
|
providerDefinitions={providerDefinitions}
|
|
favoriteKeys={favoriteKeys}
|
|
disabled={disabled}
|
|
isModelLoading={isModelLoading}
|
|
canSelectMode={canSelectMode}
|
|
canSelectModel={canSelectModel}
|
|
canSelectThinking={canSelectThinking}
|
|
canSelectProviderInModelMenu={canSelectProviderInModelMenu}
|
|
modelDisabled={modelDisabled}
|
|
effectiveProviderDefinitions={effectiveProviderDefinitions}
|
|
effectiveAllProviderModels={effectiveAllProviderModels}
|
|
displayMode={displayMode}
|
|
displayModel={displayModel}
|
|
displayThinking={displayThinking}
|
|
ModeIconComponent={ModeIconComponent}
|
|
modeIconColor={modeIconColor}
|
|
openSelector={openSelector}
|
|
ProviderIcon={ProviderIcon}
|
|
prefsOpen={prefsOpen}
|
|
handleOpenPrefs={handleOpenPrefs}
|
|
handleClosePrefs={handleClosePrefs}
|
|
prefsButtonStyle={prefsButtonStyle}
|
|
sheetThinkingPressableStyle={sheetThinkingPressableStyle}
|
|
sheetModePressableStyle={sheetModePressableStyle}
|
|
handleSheetModelSelect={handleSheetModelSelect}
|
|
handleThinkingOpenChange={handleThinkingOpenChange}
|
|
handleModeOpenChange={handleModeOpenChange}
|
|
handleOpenChange={handleOpenChange}
|
|
renderSheetModelTrigger={renderSheetModelTrigger}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface DesktopStatusBarContentProps {
|
|
provider: string;
|
|
providerOptions?: StatusOption[];
|
|
selectedProviderId?: string;
|
|
modeOptions?: StatusOption[];
|
|
selectedModeId?: string;
|
|
modelOptions?: StatusOption[];
|
|
selectedModelId?: string;
|
|
thinkingOptions?: StatusOption[];
|
|
selectedThinkingOptionId?: string;
|
|
features?: AgentFeature[];
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
onToggleFavoriteModel?: (provider: string, modelId: string) => void;
|
|
onDropdownClose?: () => void;
|
|
onModelSelectorOpen?: () => void;
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
favoriteKeys: Set<string>;
|
|
disabled: boolean;
|
|
isModelLoading: boolean;
|
|
canSelectProvider: boolean;
|
|
canSelectMode: boolean;
|
|
canSelectModel: boolean;
|
|
canSelectThinking: boolean;
|
|
canSelectProviderInModelMenu: (providerId: string) => boolean;
|
|
modelDisabled: boolean;
|
|
effectiveProviderDefinitions: AgentProviderDefinition[];
|
|
effectiveAllProviderModels: Map<string, AgentModelDefinition[]>;
|
|
comboboxProviderOptions: ComboboxOption[];
|
|
comboboxModeOptions: ComboboxOption[];
|
|
comboboxThinkingOptions: ComboboxOption[];
|
|
displayProvider: string;
|
|
displayModel: string;
|
|
displayThinking: string;
|
|
ModeIconComponent: (typeof MODE_ICONS)[keyof typeof MODE_ICONS] | null;
|
|
modeIconColor: string;
|
|
openSelector: StatusSelector | null;
|
|
providerAnchorRef: RefObject<View | null>;
|
|
thinkingAnchorRef: RefObject<View | null>;
|
|
modeAnchorRef: RefObject<View | null>;
|
|
providerPressableStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
thinkingPressableStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
modePressableStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
handleProviderPress: () => void;
|
|
handleThinkingPress: () => void;
|
|
handleModePress: () => void;
|
|
handleProviderSelect: (id: string) => void;
|
|
handleThinkingSelect: (id: string) => void;
|
|
handleModeSelect: (id: string) => void;
|
|
handleDesktopModelSelect: (providerId: string, modelId: string) => void;
|
|
handleProviderOpenChange: (open: boolean) => void;
|
|
handleThinkingOpenChange: (open: boolean) => void;
|
|
handleModeOpenChange: (open: boolean) => void;
|
|
handleOpenChange: (selector: StatusSelector) => (nextOpen: boolean) => void;
|
|
renderModeOption: (args: {
|
|
option: ComboboxOption;
|
|
selected: boolean;
|
|
active: boolean;
|
|
onPress: () => void;
|
|
}) => ReactElement;
|
|
}
|
|
|
|
const DESKTOP_SEARCH_THRESHOLD = 6;
|
|
|
|
function DesktopStatusBarContent(props: DesktopStatusBarContentProps) {
|
|
const { theme } = useUnistyles();
|
|
const {
|
|
provider,
|
|
providerOptions,
|
|
selectedProviderId,
|
|
modeOptions,
|
|
selectedModeId,
|
|
selectedModelId,
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
features,
|
|
onSetFeature,
|
|
onToggleFavoriteModel,
|
|
onDropdownClose,
|
|
onModelSelectorOpen,
|
|
favoriteKeys,
|
|
disabled,
|
|
isModelLoading,
|
|
canSelectProvider,
|
|
canSelectMode,
|
|
canSelectModel,
|
|
canSelectThinking,
|
|
canSelectProviderInModelMenu,
|
|
modelDisabled,
|
|
effectiveProviderDefinitions,
|
|
effectiveAllProviderModels,
|
|
comboboxProviderOptions,
|
|
comboboxModeOptions,
|
|
comboboxThinkingOptions,
|
|
displayProvider,
|
|
displayModel,
|
|
displayThinking,
|
|
ModeIconComponent,
|
|
modeIconColor,
|
|
openSelector,
|
|
providerAnchorRef,
|
|
thinkingAnchorRef,
|
|
modeAnchorRef,
|
|
providerPressableStyle,
|
|
thinkingPressableStyle,
|
|
modePressableStyle,
|
|
handleProviderPress,
|
|
handleThinkingPress,
|
|
handleModePress,
|
|
handleProviderSelect,
|
|
handleThinkingSelect,
|
|
handleModeSelect,
|
|
handleDesktopModelSelect,
|
|
handleProviderOpenChange,
|
|
handleThinkingOpenChange,
|
|
handleModeOpenChange,
|
|
handleOpenChange,
|
|
renderModeOption,
|
|
} = props;
|
|
|
|
return (
|
|
<>
|
|
{providerOptions && providerOptions.length > 0 ? (
|
|
<>
|
|
<Pressable
|
|
ref={providerAnchorRef}
|
|
collapsable={false}
|
|
disabled={disabled || !canSelectProvider}
|
|
onPress={handleProviderPress}
|
|
style={providerPressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Select agent provider"
|
|
testID="agent-provider-selector"
|
|
>
|
|
<Text style={styles.modeBadgeText}>{displayProvider}</Text>
|
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
<Combobox
|
|
options={comboboxProviderOptions}
|
|
value={selectedProviderId ?? ""}
|
|
onSelect={handleProviderSelect}
|
|
searchable={comboboxProviderOptions.length > DESKTOP_SEARCH_THRESHOLD}
|
|
open={openSelector === "provider"}
|
|
onOpenChange={handleProviderOpenChange}
|
|
anchorRef={providerAnchorRef}
|
|
desktopPlacement="top-start"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{canSelectModel ? (
|
|
<Tooltip
|
|
key={`model-${displayModel}`}
|
|
delayDuration={0}
|
|
enabledOnDesktop
|
|
enabledOnMobile={false}
|
|
>
|
|
<TooltipTrigger asChild triggerRefProp="ref">
|
|
<View>
|
|
<CombinedModelSelector
|
|
providerDefinitions={effectiveProviderDefinitions}
|
|
allProviderModels={effectiveAllProviderModels}
|
|
selectedProvider={provider}
|
|
selectedModel={selectedModelId ?? ""}
|
|
canSelectProvider={canSelectProviderInModelMenu}
|
|
onSelect={handleDesktopModelSelect}
|
|
favoriteKeys={favoriteKeys}
|
|
onToggleFavorite={onToggleFavoriteModel}
|
|
isLoading={isModelLoading}
|
|
disabled={modelDisabled}
|
|
onOpen={onModelSelectorOpen}
|
|
onClose={onDropdownClose}
|
|
/>
|
|
</View>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" align="center" offset={8}>
|
|
<Text style={styles.tooltipText}>{getStatusSelectorHint("model")}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
|
|
{thinkingOptions && thinkingOptions.length > 0 ? (
|
|
<>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="ref">
|
|
<Pressable
|
|
ref={thinkingAnchorRef}
|
|
collapsable={false}
|
|
disabled={disabled || !canSelectThinking}
|
|
onPress={handleThinkingPress}
|
|
style={thinkingPressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Select thinking option (${displayThinking})`}
|
|
testID="agent-thinking-selector"
|
|
>
|
|
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</Pressable>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" align="center" offset={8}>
|
|
<Text style={styles.tooltipText}>{getStatusSelectorHint("thinking")}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Combobox
|
|
options={comboboxThinkingOptions}
|
|
value={selectedThinkingOptionId ?? ""}
|
|
onSelect={handleThinkingSelect}
|
|
searchable={comboboxThinkingOptions.length > DESKTOP_SEARCH_THRESHOLD}
|
|
open={openSelector === "thinking"}
|
|
onOpenChange={handleThinkingOpenChange}
|
|
anchorRef={thinkingAnchorRef}
|
|
desktopPlacement="top-start"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{modeOptions && modeOptions.length > 0 ? (
|
|
<>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="ref">
|
|
<Pressable
|
|
ref={modeAnchorRef}
|
|
collapsable={false}
|
|
disabled={disabled || !canSelectMode}
|
|
onPress={handleModePress}
|
|
style={modePressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Select agent mode (${selectedModeId ?? ""})`}
|
|
testID="agent-mode-selector"
|
|
>
|
|
{ModeIconComponent ? (
|
|
<ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
|
|
) : (
|
|
<ShieldCheck size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
)}
|
|
</Pressable>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" align="center" offset={8}>
|
|
<Text style={styles.tooltipText}>{getStatusSelectorHint("mode")}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Combobox
|
|
options={comboboxModeOptions}
|
|
value={selectedModeId ?? ""}
|
|
onSelect={handleModeSelect}
|
|
searchable={comboboxModeOptions.length > DESKTOP_SEARCH_THRESHOLD}
|
|
open={openSelector === "mode"}
|
|
onOpenChange={handleModeOpenChange}
|
|
anchorRef={modeAnchorRef}
|
|
desktopPlacement="top-start"
|
|
renderOption={renderModeOption}
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{features?.map((feature) => (
|
|
<DesktopFeatureItem
|
|
key={`feature-${feature.id}`}
|
|
feature={feature}
|
|
disabled={disabled}
|
|
openSelector={openSelector}
|
|
handleOpenChange={handleOpenChange}
|
|
onSetFeature={onSetFeature}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface SheetStatusBarContentProps {
|
|
provider: string;
|
|
modeOptions?: StatusOption[];
|
|
selectedModeId?: string;
|
|
selectedModelId?: string;
|
|
thinkingOptions?: StatusOption[];
|
|
selectedThinkingOptionId?: string;
|
|
features?: AgentFeature[];
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
onSelectMode?: (modeId: string) => void;
|
|
onSelectThinkingOption?: (thinkingOptionId: string) => void;
|
|
onToggleFavoriteModel?: (provider: string, modelId: string) => void;
|
|
onDropdownClose?: () => void;
|
|
onModelSelectorOpen?: () => void;
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
favoriteKeys: Set<string>;
|
|
disabled: boolean;
|
|
isModelLoading: boolean;
|
|
canSelectMode: boolean;
|
|
canSelectModel: boolean;
|
|
canSelectThinking: boolean;
|
|
canSelectProviderInModelMenu: (providerId: string) => boolean;
|
|
modelDisabled: boolean;
|
|
effectiveProviderDefinitions: AgentProviderDefinition[];
|
|
effectiveAllProviderModels: Map<string, AgentModelDefinition[]>;
|
|
displayMode: string;
|
|
displayModel: string;
|
|
displayThinking: string;
|
|
ModeIconComponent: (typeof MODE_ICONS)[keyof typeof MODE_ICONS] | null;
|
|
modeIconColor: string;
|
|
openSelector: StatusSelector | null;
|
|
ProviderIcon: ReturnType<typeof getProviderIcon> | null;
|
|
prefsOpen: boolean;
|
|
handleOpenPrefs: () => void;
|
|
handleClosePrefs: () => void;
|
|
prefsButtonStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
sheetThinkingPressableStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
sheetModePressableStyle: (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
|
handleSheetModelSelect: (providerId: string, modelId: string) => void;
|
|
handleThinkingOpenChange: (open: boolean) => void;
|
|
handleModeOpenChange: (open: boolean) => void;
|
|
handleOpenChange: (selector: StatusSelector) => (nextOpen: boolean) => void;
|
|
renderSheetModelTrigger: (args: { selectedModelLabel: string }) => ReactElement;
|
|
}
|
|
|
|
function SheetStatusBarContent(props: SheetStatusBarContentProps) {
|
|
const { theme } = useUnistyles();
|
|
const {
|
|
provider,
|
|
modeOptions,
|
|
selectedModeId,
|
|
selectedModelId,
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
features,
|
|
onSetFeature,
|
|
onSelectMode,
|
|
onSelectThinkingOption,
|
|
onToggleFavoriteModel,
|
|
onDropdownClose,
|
|
onModelSelectorOpen,
|
|
providerDefinitions,
|
|
favoriteKeys,
|
|
disabled,
|
|
isModelLoading,
|
|
canSelectMode,
|
|
canSelectModel,
|
|
canSelectThinking,
|
|
canSelectProviderInModelMenu,
|
|
modelDisabled,
|
|
effectiveProviderDefinitions,
|
|
effectiveAllProviderModels,
|
|
displayMode,
|
|
displayModel,
|
|
displayThinking,
|
|
ModeIconComponent,
|
|
modeIconColor,
|
|
openSelector,
|
|
ProviderIcon,
|
|
prefsOpen,
|
|
handleOpenPrefs,
|
|
handleClosePrefs,
|
|
prefsButtonStyle,
|
|
sheetThinkingPressableStyle,
|
|
sheetModePressableStyle,
|
|
handleSheetModelSelect,
|
|
handleThinkingOpenChange,
|
|
handleModeOpenChange,
|
|
handleOpenChange,
|
|
renderSheetModelTrigger,
|
|
} = props;
|
|
|
|
return (
|
|
<>
|
|
<Pressable
|
|
onPress={handleOpenPrefs}
|
|
style={prefsButtonStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Agent preferences"
|
|
testID="agent-preferences-button"
|
|
>
|
|
{ProviderIcon ? (
|
|
<ProviderIcon size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
|
|
) : null}
|
|
<Text style={styles.prefsButtonText} numberOfLines={1}>
|
|
{displayModel}
|
|
</Text>
|
|
</Pressable>
|
|
|
|
<AdaptiveModalSheet
|
|
title="Preferences"
|
|
visible={prefsOpen}
|
|
onClose={handleClosePrefs}
|
|
testID="agent-preferences-sheet"
|
|
>
|
|
{canSelectModel ? (
|
|
<View style={styles.sheetSection}>
|
|
<CombinedModelSelector
|
|
providerDefinitions={effectiveProviderDefinitions}
|
|
allProviderModels={effectiveAllProviderModels}
|
|
selectedProvider={provider}
|
|
selectedModel={selectedModelId ?? ""}
|
|
canSelectProvider={canSelectProviderInModelMenu}
|
|
onSelect={handleSheetModelSelect}
|
|
favoriteKeys={favoriteKeys}
|
|
onToggleFavorite={onToggleFavoriteModel}
|
|
isLoading={isModelLoading}
|
|
disabled={modelDisabled}
|
|
onOpen={onModelSelectorOpen}
|
|
onClose={onDropdownClose}
|
|
renderTrigger={renderSheetModelTrigger}
|
|
/>
|
|
</View>
|
|
) : null}
|
|
|
|
{thinkingOptions && thinkingOptions.length > 0 ? (
|
|
<View style={styles.sheetSection}>
|
|
<DropdownMenu
|
|
open={openSelector === "thinking"}
|
|
onOpenChange={handleThinkingOpenChange}
|
|
>
|
|
<DropdownMenuTrigger
|
|
disabled={disabled || !canSelectThinking}
|
|
style={sheetThinkingPressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Select thinking option"
|
|
testID="agent-preferences-thinking"
|
|
>
|
|
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="top" align="start">
|
|
{thinkingOptions.map((thinking) => (
|
|
<ThinkingMenuItem
|
|
key={thinking.id}
|
|
thinking={thinking}
|
|
selected={thinking.id === selectedThinkingOptionId}
|
|
onSelectThinkingOption={onSelectThinkingOption}
|
|
/>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</View>
|
|
) : null}
|
|
|
|
{modeOptions && modeOptions.length > 0 ? (
|
|
<View style={styles.sheetSection}>
|
|
<DropdownMenu open={openSelector === "mode"} onOpenChange={handleModeOpenChange}>
|
|
<DropdownMenuTrigger
|
|
disabled={disabled || !canSelectMode}
|
|
style={sheetModePressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Select agent mode"
|
|
testID="agent-preferences-mode"
|
|
>
|
|
{ModeIconComponent ? (
|
|
<ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
|
|
) : null}
|
|
<Text style={styles.sheetSelectText}>{displayMode}</Text>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="top" align="start">
|
|
{modeOptions.map((mode) => (
|
|
<ModeMenuItem
|
|
key={mode.id}
|
|
mode={mode}
|
|
provider={provider}
|
|
providerDefinitions={providerDefinitions}
|
|
selected={mode.id === selectedModeId}
|
|
onSelectMode={onSelectMode}
|
|
/>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</View>
|
|
) : null}
|
|
|
|
{features?.map((feature) => (
|
|
<SheetFeatureItem
|
|
key={`feature-${feature.id}`}
|
|
feature={feature}
|
|
disabled={disabled}
|
|
openSelector={openSelector}
|
|
handleOpenChange={handleOpenChange}
|
|
onSetFeature={onSetFeature}
|
|
/>
|
|
))}
|
|
</AdaptiveModalSheet>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function DesktopFeatureItem({
|
|
feature,
|
|
disabled,
|
|
openSelector,
|
|
handleOpenChange,
|
|
onSetFeature,
|
|
}: {
|
|
feature: AgentFeature;
|
|
disabled: boolean;
|
|
openSelector: StatusSelector | null;
|
|
handleOpenChange: (selector: StatusSelector) => (nextOpen: boolean) => void;
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const featureSelector: StatusSelector = `feature-${feature.id}`;
|
|
|
|
const handleFeatureOpenChange = useMemo(
|
|
() => handleOpenChange(featureSelector),
|
|
[handleOpenChange, featureSelector],
|
|
);
|
|
|
|
const handleTogglePress = useCallback(() => {
|
|
if (feature.type === "toggle") {
|
|
onSetFeature?.(feature.id, !feature.value);
|
|
}
|
|
}, [feature, onSetFeature]);
|
|
|
|
const handleSelectOption = useCallback(
|
|
(optionId: string) => {
|
|
onSetFeature?.(feature.id, optionId);
|
|
},
|
|
[feature.id, onSetFeature],
|
|
);
|
|
|
|
const togglePressableStyle = useCallback(
|
|
({ pressed, hovered }: PressableStateCallbackType) => [
|
|
styles.modeIconBadge,
|
|
hovered && styles.modeBadgeHovered,
|
|
pressed && styles.modeBadgePressed,
|
|
disabled && styles.disabledBadge,
|
|
],
|
|
[disabled],
|
|
);
|
|
|
|
const selectPressableStyle = useCallback(
|
|
({ pressed, hovered }: PressableStateCallbackType) => [
|
|
styles.modeBadge,
|
|
hovered && styles.modeBadgeHovered,
|
|
(pressed || openSelector === featureSelector) && styles.modeBadgePressed,
|
|
disabled && styles.disabledBadge,
|
|
],
|
|
[disabled, openSelector, featureSelector],
|
|
);
|
|
|
|
if (feature.type === "toggle") {
|
|
const FeatureIcon = getFeatureIcon(feature.icon);
|
|
return (
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="ref">
|
|
<Pressable
|
|
disabled={disabled}
|
|
onPress={handleTogglePress}
|
|
style={togglePressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={getFeatureTooltip(feature)}
|
|
testID={`agent-feature-${feature.id}`}
|
|
>
|
|
<FeatureIcon
|
|
size={theme.iconSize.md}
|
|
color={getFeatureIconColor(
|
|
feature.id,
|
|
feature.value,
|
|
theme.colors.palette,
|
|
theme.colors.foregroundMuted,
|
|
)}
|
|
/>
|
|
</Pressable>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" align="center" offset={8}>
|
|
<Text style={styles.tooltipText}>{getFeatureTooltip(feature)}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
if (feature.type === "select") {
|
|
const FeatureIcon = getFeatureIcon(feature.icon);
|
|
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
|
return (
|
|
<DropdownMenu open={openSelector === featureSelector} onOpenChange={handleFeatureOpenChange}>
|
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild triggerRefProp="ref">
|
|
<DropdownMenuTrigger
|
|
disabled={disabled}
|
|
style={selectPressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={getFeatureTooltip(feature)}
|
|
testID={`agent-feature-${feature.id}`}
|
|
>
|
|
<FeatureIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
<Text style={styles.modeBadgeText}>{selectedOption?.label ?? feature.label}</Text>
|
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" align="center" offset={8}>
|
|
<Text style={styles.tooltipText}>{getFeatureTooltip(feature)}</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent side="top" align="start">
|
|
{feature.options.map((option) => (
|
|
<FeatureOptionMenuItem
|
|
key={option.id}
|
|
option={option}
|
|
selected={option.id === feature.value}
|
|
onSelect={handleSelectOption}
|
|
/>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function SheetFeatureItem({
|
|
feature,
|
|
disabled,
|
|
openSelector,
|
|
handleOpenChange,
|
|
onSetFeature,
|
|
}: {
|
|
feature: AgentFeature;
|
|
disabled: boolean;
|
|
openSelector: StatusSelector | null;
|
|
handleOpenChange: (selector: StatusSelector) => (nextOpen: boolean) => void;
|
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const featureSelector: StatusSelector = `feature-${feature.id}`;
|
|
|
|
const handleFeatureOpenChange = useMemo(
|
|
() => handleOpenChange(featureSelector),
|
|
[handleOpenChange, featureSelector],
|
|
);
|
|
|
|
const handleTogglePress = useCallback(() => {
|
|
if (feature.type === "toggle") {
|
|
onSetFeature?.(feature.id, !feature.value);
|
|
}
|
|
}, [feature, onSetFeature]);
|
|
|
|
const handleSelectOption = useCallback(
|
|
(optionId: string) => {
|
|
onSetFeature?.(feature.id, optionId);
|
|
},
|
|
[feature.id, onSetFeature],
|
|
);
|
|
|
|
const togglePressableStyle = useCallback(
|
|
({ pressed }: PressableStateCallbackType) => [
|
|
styles.sheetSelect,
|
|
pressed && styles.sheetSelectPressed,
|
|
disabled && styles.disabledSheetSelect,
|
|
],
|
|
[disabled],
|
|
);
|
|
|
|
if (feature.type === "toggle") {
|
|
const FeatureIcon = getFeatureIcon(feature.icon);
|
|
return (
|
|
<View style={styles.sheetSection}>
|
|
<Pressable
|
|
disabled={disabled}
|
|
onPress={handleTogglePress}
|
|
style={togglePressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={getFeatureTooltip(feature)}
|
|
testID={`agent-feature-${feature.id}`}
|
|
>
|
|
<FeatureIcon
|
|
size={theme.iconSize.md}
|
|
color={getFeatureIconColor(
|
|
feature.id,
|
|
feature.value,
|
|
theme.colors.palette,
|
|
theme.colors.foregroundMuted,
|
|
)}
|
|
/>
|
|
<Text style={styles.sheetSelectText}>{feature.label}</Text>
|
|
<Text style={styles.modeBadgeText}>{feature.value ? "On" : "Off"}</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (feature.type === "select") {
|
|
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
|
return (
|
|
<View style={styles.sheetSection}>
|
|
<DropdownMenu
|
|
open={openSelector === featureSelector}
|
|
onOpenChange={handleFeatureOpenChange}
|
|
>
|
|
<DropdownMenuTrigger
|
|
disabled={disabled}
|
|
style={togglePressableStyle}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={getFeatureTooltip(feature)}
|
|
testID={`agent-feature-${feature.id}`}
|
|
>
|
|
<Text style={styles.sheetSelectText}>{selectedOption?.label ?? feature.label}</Text>
|
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="top" align="start">
|
|
{feature.options.map((option) => (
|
|
<FeatureOptionMenuItem
|
|
key={option.id}
|
|
option={option}
|
|
selected={option.id === feature.value}
|
|
onSelect={handleSelectOption}
|
|
/>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function FeatureOptionMenuItem({
|
|
option,
|
|
selected,
|
|
onSelect,
|
|
}: {
|
|
option: { id: string; label: string };
|
|
selected: boolean;
|
|
onSelect: (optionId: string) => void;
|
|
}) {
|
|
const handleSelect = useCallback(() => {
|
|
onSelect(option.id);
|
|
}, [onSelect, option.id]);
|
|
|
|
return (
|
|
<DropdownMenuItem selected={selected} onSelect={handleSelect}>
|
|
{option.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
}
|
|
|
|
function ThinkingMenuItem({
|
|
thinking,
|
|
selected,
|
|
onSelectThinkingOption,
|
|
}: {
|
|
thinking: StatusOption;
|
|
selected: boolean;
|
|
onSelectThinkingOption?: (thinkingOptionId: string) => void;
|
|
}) {
|
|
const handleSelect = useCallback(() => {
|
|
onSelectThinkingOption?.(thinking.id);
|
|
}, [onSelectThinkingOption, thinking.id]);
|
|
|
|
return (
|
|
<DropdownMenuItem selected={selected} onSelect={handleSelect}>
|
|
{thinking.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
}
|
|
|
|
function ModeComboboxOption({
|
|
option,
|
|
selected,
|
|
active,
|
|
onPress,
|
|
provider,
|
|
providerDefinitions,
|
|
iconColor,
|
|
}: {
|
|
option: ComboboxOption;
|
|
selected: boolean;
|
|
active: boolean;
|
|
onPress: () => void;
|
|
provider: string;
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
iconColor: string;
|
|
}) {
|
|
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
|
|
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
|
|
const leadingSlot = useMemo(
|
|
() => <IconComponent size={16} color={iconColor} />,
|
|
[IconComponent, iconColor],
|
|
);
|
|
return (
|
|
<ComboboxItem
|
|
label={option.label}
|
|
selected={selected}
|
|
active={active}
|
|
onPress={onPress}
|
|
leadingSlot={leadingSlot}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ModeMenuItem({
|
|
mode,
|
|
provider,
|
|
providerDefinitions,
|
|
selected,
|
|
onSelectMode,
|
|
}: {
|
|
mode: StatusOption;
|
|
provider: string;
|
|
providerDefinitions: AgentProviderDefinition[];
|
|
selected: boolean;
|
|
onSelectMode?: (modeId: string) => void;
|
|
}) {
|
|
const { theme } = useUnistyles();
|
|
const visuals = getModeVisuals(provider, mode.id, providerDefinitions);
|
|
const Icon = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
|
|
|
|
const handleSelect = useCallback(() => {
|
|
onSelectMode?.(mode.id);
|
|
}, [mode.id, onSelectMode]);
|
|
|
|
const leadingIcon = useMemo(
|
|
() => <Icon size={16} color={theme.colors.foreground} />,
|
|
[Icon, theme.colors.foreground],
|
|
);
|
|
|
|
return (
|
|
<DropdownMenuItem selected={selected} onSelect={handleSelect} leading={leadingIcon}>
|
|
{mode.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
}
|
|
|
|
const EMPTY_MODES: AgentMode[] = [];
|
|
|
|
export const AgentStatusBar = memo(function AgentStatusBar({
|
|
agentId,
|
|
serverId,
|
|
onDropdownClose,
|
|
}: AgentStatusBarProps) {
|
|
const { preferences, updatePreferences } = useFormPreferences();
|
|
const agent = useSessionStore(
|
|
useShallow((state) => selectAgentStatusBarSlice(state, serverId, agentId)),
|
|
);
|
|
const availableModes = useStoreWithEqualityFn(
|
|
useSessionStore,
|
|
(state) => state.sessions[serverId]?.agents?.get(agentId)?.availableModes ?? EMPTY_MODES,
|
|
compareAvailableModes,
|
|
);
|
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
|
const toast = useToast();
|
|
|
|
const {
|
|
entries: snapshotEntries,
|
|
isLoading: snapshotIsLoading,
|
|
refetchIfStale: refetchSnapshotIfStale,
|
|
} = useProvidersSnapshot(serverId);
|
|
|
|
const snapshotSelectedEntry = useMemo(
|
|
() => resolveSnapshotSelectedEntry(snapshotEntries, agent?.provider),
|
|
[snapshotEntries, agent?.provider],
|
|
);
|
|
|
|
const models = snapshotSelectedEntry?.models ?? null;
|
|
const selectedProviderIsLoading = snapshotSelectedEntry?.status === "loading";
|
|
|
|
const agentProviderDefinitions = useMemo(
|
|
() => buildAgentProviderDefinitions(agent?.provider, snapshotEntries),
|
|
[agent?.provider, snapshotEntries],
|
|
);
|
|
|
|
const agentProviderModels = useMemo(
|
|
() => buildAgentProviderModels(agent?.provider, models),
|
|
[agent?.provider, models],
|
|
);
|
|
|
|
const displayMode = resolveAgentDisplayMode(availableModes, agent?.currentModeId);
|
|
|
|
const modelSelection = resolveAgentModelSelection({
|
|
models,
|
|
runtimeModelId: agent?.runtimeModelId,
|
|
configuredModelId: agent?.model,
|
|
explicitThinkingOptionId: agent?.thinkingOptionId,
|
|
});
|
|
|
|
const modeOptions = useMemo<StatusOption[]>(() => {
|
|
return availableModes.map((mode) => ({
|
|
id: mode.id,
|
|
label: mode.label,
|
|
}));
|
|
}, [availableModes]);
|
|
|
|
const modelOptions = useMemo<StatusOption[]>(() => {
|
|
return (models ?? []).map((model) => ({ id: model.id, label: model.label }));
|
|
}, [models]);
|
|
const favoriteKeys = useMemo(
|
|
() =>
|
|
new Set(
|
|
(preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)),
|
|
),
|
|
[preferences.favoriteModels],
|
|
);
|
|
|
|
const thinkingOptions = useMemo<StatusOption[]>(() => {
|
|
return (modelSelection.thinkingOptions ?? []).map((option) => ({
|
|
id: option.id,
|
|
label: option.label,
|
|
}));
|
|
}, [modelSelection.thinkingOptions]);
|
|
|
|
const agentProvider = agent?.provider;
|
|
const activeModelId = modelSelection.activeModelId;
|
|
|
|
const handleSelectMode = useCallback(
|
|
(modeId: string) => {
|
|
if (!client) {
|
|
return;
|
|
}
|
|
void client.setAgentMode(agentId, modeId).catch((error) => {
|
|
console.warn("[AgentStatusBar] setAgentMode failed", error);
|
|
toast.error(toErrorMessage(error));
|
|
});
|
|
},
|
|
[agentId, client, toast],
|
|
);
|
|
|
|
const handleSelectModel = useCallback(
|
|
(modelId: string) => {
|
|
if (!client || !agentProvider) {
|
|
return;
|
|
}
|
|
void updatePreferences((current) =>
|
|
mergeProviderPreferences({
|
|
preferences: current,
|
|
provider: agentProvider,
|
|
updates: {
|
|
model: modelId,
|
|
},
|
|
}),
|
|
).catch((error) => {
|
|
console.warn("[AgentStatusBar] persist model preference failed", error);
|
|
});
|
|
void client.setAgentModel(agentId, modelId).catch((error) => {
|
|
console.warn("[AgentStatusBar] setAgentModel failed", error);
|
|
toast.error(toErrorMessage(error));
|
|
});
|
|
},
|
|
[agentId, agentProvider, client, toast, updatePreferences],
|
|
);
|
|
|
|
const handleToggleFavoriteModel = useCallback(
|
|
(provider: string, modelId: string) => {
|
|
void updatePreferences((current) =>
|
|
toggleFavoriteModel({ preferences: current, provider, modelId }),
|
|
).catch((error) => {
|
|
console.warn("[AgentStatusBar] toggle favorite model failed", error);
|
|
});
|
|
},
|
|
[updatePreferences],
|
|
);
|
|
|
|
const handleSelectThinkingOption = useCallback(
|
|
(thinkingOptionId: string) => {
|
|
if (!client || !agentProvider) {
|
|
return;
|
|
}
|
|
if (activeModelId) {
|
|
void updatePreferences((current) =>
|
|
mergeProviderPreferences({
|
|
preferences: current,
|
|
provider: agentProvider,
|
|
updates: {
|
|
model: activeModelId,
|
|
thinkingByModel: {
|
|
[activeModelId]: thinkingOptionId,
|
|
},
|
|
},
|
|
}),
|
|
).catch((error) => {
|
|
console.warn("[AgentStatusBar] persist thinking preference failed", error);
|
|
});
|
|
}
|
|
void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => {
|
|
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
|
toast.error(toErrorMessage(error));
|
|
});
|
|
},
|
|
[activeModelId, agentId, agentProvider, client, toast, updatePreferences],
|
|
);
|
|
|
|
const handleSetFeature = useCallback(
|
|
(featureId: string, value: unknown) => {
|
|
if (!client || !agentProvider) {
|
|
return;
|
|
}
|
|
void updatePreferences((current) =>
|
|
mergeProviderPreferences({
|
|
preferences: current,
|
|
provider: agentProvider,
|
|
updates: {
|
|
featureValues: {
|
|
[featureId]: value,
|
|
},
|
|
},
|
|
}),
|
|
).catch((error) => {
|
|
console.warn("[AgentStatusBar] persist feature preference failed", error);
|
|
});
|
|
void client.setAgentFeature(agentId, featureId, value).catch((error) => {
|
|
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
|
toast.error(toErrorMessage(error));
|
|
});
|
|
},
|
|
[agentId, agentProvider, client, toast, updatePreferences],
|
|
);
|
|
|
|
const handleModelSelectorOpen = useCallback(() => {
|
|
refetchSnapshotIfStale(agentProvider);
|
|
}, [agentProvider, refetchSnapshotIfStale]);
|
|
|
|
const fallbackModeOptions = useMemo<StatusOption[]>(
|
|
() =>
|
|
modeOptions.length > 0
|
|
? modeOptions
|
|
: [{ id: agent?.currentModeId ?? "", label: displayMode }],
|
|
[agent?.currentModeId, displayMode, modeOptions],
|
|
);
|
|
|
|
if (!agent) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ControlledStatusBar
|
|
provider={agent.provider}
|
|
modeOptions={fallbackModeOptions}
|
|
selectedModeId={agent.currentModeId ?? undefined}
|
|
providerDefinitions={agentProviderDefinitions}
|
|
allProviderModels={agentProviderModels}
|
|
onSelectMode={handleSelectMode}
|
|
modelOptions={modelOptions}
|
|
selectedModelId={modelSelection.activeModelId ?? undefined}
|
|
onSelectModel={handleSelectModel}
|
|
favoriteKeys={favoriteKeys}
|
|
onToggleFavoriteModel={handleToggleFavoriteModel}
|
|
thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined}
|
|
selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined}
|
|
onSelectThinkingOption={handleSelectThinkingOption}
|
|
features={agent.features}
|
|
onSetFeature={handleSetFeature}
|
|
isModelLoading={snapshotIsLoading || selectedProviderIsLoading}
|
|
onModelSelectorOpen={handleModelSelectorOpen}
|
|
onDropdownClose={onDropdownClose}
|
|
disabled={!client}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export function DraftAgentStatusBar({
|
|
providerDefinitions,
|
|
selectedProvider,
|
|
onSelectProvider: _onSelectProvider,
|
|
modeOptions,
|
|
selectedMode,
|
|
onSelectMode,
|
|
models,
|
|
selectedModel,
|
|
onSelectModel,
|
|
isModelLoading: _isModelLoading,
|
|
allProviderModels,
|
|
isAllModelsLoading,
|
|
onSelectProviderAndModel,
|
|
thinkingOptions,
|
|
selectedThinkingOptionId,
|
|
onSelectThinkingOption,
|
|
features,
|
|
onSetFeature,
|
|
onDropdownClose,
|
|
onModelSelectorOpen,
|
|
disabled = false,
|
|
}: DraftAgentStatusBarProps) {
|
|
const { preferences, updatePreferences } = useFormPreferences();
|
|
|
|
const mappedModeOptions = useMemo<StatusOption[]>(() => {
|
|
if (modeOptions.length === 0) {
|
|
return [{ id: "", label: "Default" }];
|
|
}
|
|
return modeOptions.map((mode) => ({
|
|
id: mode.id,
|
|
label: mode.label,
|
|
}));
|
|
}, [modeOptions]);
|
|
|
|
const mappedThinkingOptions = useMemo<StatusOption[]>(() => {
|
|
return thinkingOptions.map((option) => ({ id: option.id, label: option.label }));
|
|
}, [thinkingOptions]);
|
|
const favoriteKeys = useMemo(
|
|
() =>
|
|
new Set(
|
|
(preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)),
|
|
),
|
|
[preferences.favoriteModels],
|
|
);
|
|
|
|
const effectiveSelectedMode = selectedMode || mappedModeOptions[0]?.id || "";
|
|
const effectiveSelectedThinkingOption =
|
|
selectedThinkingOptionId || mappedThinkingOptions[0]?.id || undefined;
|
|
const hasSelectedProvider = selectedProvider !== null;
|
|
|
|
const modelOptions = useMemo<StatusOption[]>(
|
|
() =>
|
|
models.map((model) => ({
|
|
id: model.id,
|
|
label: model.label,
|
|
})),
|
|
[models],
|
|
);
|
|
|
|
const handleToggleFavorite = useCallback(
|
|
(provider: string, modelId: string) => {
|
|
void updatePreferences((current) =>
|
|
toggleFavoriteModel({ preferences: current, provider, modelId }),
|
|
).catch((error) => {
|
|
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
|
|
});
|
|
},
|
|
[updatePreferences],
|
|
);
|
|
|
|
if (platformIsWeb) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<CombinedModelSelector
|
|
providerDefinitions={providerDefinitions}
|
|
allProviderModels={allProviderModels}
|
|
selectedProvider={selectedProvider ?? ""}
|
|
selectedModel={selectedModel}
|
|
onSelect={onSelectProviderAndModel}
|
|
favoriteKeys={favoriteKeys}
|
|
onToggleFavorite={handleToggleFavorite}
|
|
isLoading={isAllModelsLoading}
|
|
disabled={disabled}
|
|
onOpen={onModelSelectorOpen}
|
|
onClose={onDropdownClose}
|
|
/>
|
|
{selectedProvider ? (
|
|
<ControlledStatusBar
|
|
provider={selectedProvider}
|
|
providerDefinitions={providerDefinitions}
|
|
modeOptions={mappedModeOptions}
|
|
selectedModeId={effectiveSelectedMode}
|
|
onSelectMode={onSelectMode}
|
|
thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined}
|
|
selectedThinkingOptionId={effectiveSelectedThinkingOption}
|
|
onSelectThinkingOption={onSelectThinkingOption}
|
|
features={features}
|
|
onSetFeature={onSetFeature}
|
|
onDropdownClose={onDropdownClose}
|
|
disabled={disabled}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ControlledStatusBar
|
|
provider={selectedProvider ?? ""}
|
|
providerDefinitions={providerDefinitions}
|
|
allProviderModels={allProviderModels}
|
|
modeOptions={hasSelectedProvider ? mappedModeOptions : undefined}
|
|
selectedModeId={effectiveSelectedMode}
|
|
onSelectMode={onSelectMode}
|
|
modelOptions={modelOptions}
|
|
selectedModelId={selectedModel}
|
|
onSelectModel={onSelectModel}
|
|
onSelectProviderAndModel={onSelectProviderAndModel}
|
|
isModelLoading={isAllModelsLoading}
|
|
favoriteKeys={favoriteKeys}
|
|
onToggleFavoriteModel={handleToggleFavorite}
|
|
thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined}
|
|
selectedThinkingOptionId={effectiveSelectedThinkingOption}
|
|
onSelectThinkingOption={onSelectThinkingOption}
|
|
features={features}
|
|
onSetFeature={onSetFeature}
|
|
onModelSelectorOpen={onModelSelectorOpen}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-end",
|
|
gap: theme.spacing[1],
|
|
},
|
|
modeBadge: {
|
|
height: 28,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
backgroundColor: "transparent",
|
|
gap: theme.spacing[1],
|
|
paddingHorizontal: theme.spacing[2],
|
|
borderRadius: theme.borderRadius["2xl"],
|
|
},
|
|
modeIconBadge: {
|
|
width: 28,
|
|
height: 28,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
backgroundColor: "transparent",
|
|
borderRadius: theme.borderRadius.full,
|
|
},
|
|
modeBadgeHovered: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
modeBadgePressed: {
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
disabledBadge: {
|
|
opacity: 0.5,
|
|
},
|
|
modeBadgeText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.normal,
|
|
},
|
|
tooltipText: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: theme.fontSize.sm * 1.4,
|
|
},
|
|
prefsButton: {
|
|
height: 28,
|
|
minWidth: 0,
|
|
flexShrink: 1,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[1],
|
|
paddingHorizontal: theme.spacing[2],
|
|
borderRadius: theme.borderRadius["2xl"],
|
|
},
|
|
prefsButtonPressed: {
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
prefsButtonText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.normal,
|
|
flexShrink: 1,
|
|
},
|
|
sheetSection: {
|
|
gap: theme.spacing[2],
|
|
},
|
|
sheetSelect: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.lg,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.surface2,
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
sheetSelectPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
disabledSheetSelect: {
|
|
opacity: 0.5,
|
|
},
|
|
sheetSelectText: {
|
|
flex: 1,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
},
|
|
}));
|