feat: add provider-declared features system with Codex fast mode (#186)
Introduce a generic feature system where providers declare dynamic features (toggles/selects) and the app renders controls automatically. One message pair (set_agent_feature_request/response) handles all feature mutations. Feature values persist and restore on agent resume. First consumer: Codex fast mode (service_tier) — gated to supported model families, with proper cleanup on model switch.
This commit is contained in:
@@ -3,7 +3,7 @@ import { View, Text, Platform, Pressable, Keyboard } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native";
|
||||
import { Brain, ChevronDown, Settings2, ShieldAlert, ShieldCheck, ShieldOff, Zap } from "lucide-react-native";
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -24,6 +24,7 @@ import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/com
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type {
|
||||
AgentFeature,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentProvider,
|
||||
@@ -46,7 +47,7 @@ type StatusOption = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type StatusSelector = "provider" | "mode" | "model" | "thinking";
|
||||
type StatusSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
|
||||
|
||||
const PROVIDER_DEFINITION_MAP = new Map(
|
||||
AGENT_PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]),
|
||||
@@ -73,6 +74,8 @@ type ControlledAgentStatusBarProps = {
|
||||
canSelectModelProvider?: (providerId: string) => boolean;
|
||||
favoriteKeys?: Set<string>;
|
||||
onToggleFavoriteModel?: (provider: string, modelId: string) => void;
|
||||
features?: AgentFeature[];
|
||||
onSetFeature?: (featureId: string, value: unknown) => void;
|
||||
};
|
||||
|
||||
export interface DraftAgentStatusBarProps {
|
||||
@@ -112,6 +115,14 @@ function findOptionLabel(
|
||||
return selected?.label ?? fallback;
|
||||
}
|
||||
|
||||
const FEATURE_ICONS: Record<string, typeof Zap> = {
|
||||
zap: Zap,
|
||||
};
|
||||
|
||||
function getFeatureIcon(icon?: string) {
|
||||
return (icon && FEATURE_ICONS[icon]) || Settings2;
|
||||
}
|
||||
|
||||
const MODE_ICONS = {
|
||||
ShieldCheck,
|
||||
ShieldAlert,
|
||||
@@ -162,6 +173,8 @@ function ControlledStatusBar({
|
||||
canSelectModelProvider,
|
||||
favoriteKeys = new Set<string>(),
|
||||
onToggleFavoriteModel,
|
||||
features,
|
||||
onSetFeature,
|
||||
}: ControlledAgentStatusBarProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const isWeb = Platform.OS === "web";
|
||||
@@ -407,6 +420,106 @@ function ControlledStatusBar({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{features?.map((feature) => {
|
||||
if (feature.type === "toggle") {
|
||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
||||
return (
|
||||
<Tooltip
|
||||
key={`feature-${feature.id}`}
|
||||
delayDuration={0}
|
||||
enabledOnDesktop
|
||||
enabledOnMobile={false}
|
||||
>
|
||||
<TooltipTrigger asChild triggerRefProp="ref">
|
||||
<Pressable
|
||||
disabled={disabled}
|
||||
onPress={() => onSetFeature?.(feature.id, !feature.value)}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.modeIconBadge,
|
||||
hovered && styles.modeBadgeHovered,
|
||||
pressed && styles.modeBadgePressed,
|
||||
disabled && styles.disabledBadge,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={feature.label}
|
||||
testID={`agent-feature-${feature.id}`}
|
||||
>
|
||||
<FeatureIcon
|
||||
size={theme.iconSize.md}
|
||||
color={
|
||||
feature.value
|
||||
? theme.colors.palette.yellow[400]
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center" offset={8}>
|
||||
<Text style={styles.tooltipText}>{feature.label}</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (feature.type === "select") {
|
||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
||||
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
||||
return (
|
||||
<DropdownMenu
|
||||
key={`feature-${feature.id}`}
|
||||
open={openSelector === `feature-${feature.id}`}
|
||||
onOpenChange={(open) =>
|
||||
setOpenSelector(open ? `feature-${feature.id}` : null)
|
||||
}
|
||||
>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger asChild triggerRefProp="ref">
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.modeBadge,
|
||||
hovered && styles.modeBadgeHovered,
|
||||
(pressed || openSelector === `feature-${feature.id}`) &&
|
||||
styles.modeBadgePressed,
|
||||
disabled && styles.disabledBadge,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={feature.label}
|
||||
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}>{feature.label}</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
{feature.options.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
selected={option.id === feature.value}
|
||||
onSelect={() => onSetFeature?.(feature.id, option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
{modeOptions && modeOptions.length > 0 ? (
|
||||
<>
|
||||
<Tooltip
|
||||
@@ -551,6 +664,86 @@ function ControlledStatusBar({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{features?.map((feature) => {
|
||||
if (feature.type === "toggle") {
|
||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
||||
return (
|
||||
<View key={`feature-${feature.id}`} style={styles.sheetSection}>
|
||||
<Pressable
|
||||
disabled={disabled}
|
||||
onPress={() => onSetFeature?.(feature.id, !feature.value)}
|
||||
style={({ pressed }) => [
|
||||
styles.sheetSelect,
|
||||
pressed && styles.sheetSelectPressed,
|
||||
disabled && styles.disabledSheetSelect,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={feature.label}
|
||||
testID={`agent-feature-${feature.id}`}
|
||||
>
|
||||
<FeatureIcon
|
||||
size={theme.iconSize.md}
|
||||
color={
|
||||
feature.value
|
||||
? theme.colors.palette.yellow[400]
|
||||
: 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 key={`feature-${feature.id}`} style={styles.sheetSection}>
|
||||
<DropdownMenu
|
||||
open={openSelector === `feature-${feature.id}`}
|
||||
onOpenChange={(open) =>
|
||||
setOpenSelector(open ? `feature-${feature.id}` : null)
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
style={({ pressed }) => [
|
||||
styles.sheetSelect,
|
||||
pressed && styles.sheetSelectPressed,
|
||||
disabled && styles.disabledSheetSelect,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={feature.label}
|
||||
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) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
selected={option.id === feature.value}
|
||||
onSelect={() => onSetFeature?.(feature.id, option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
{modeOptions && modeOptions.length > 0 ? (
|
||||
<View style={styles.sheetSection}>
|
||||
<DropdownMenu
|
||||
@@ -614,6 +807,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||
currentModeId: currentAgent.currentModeId,
|
||||
runtimeModelId: currentAgent.runtimeInfo?.model ?? null,
|
||||
model: currentAgent.model,
|
||||
features: currentAgent.features,
|
||||
thinkingOptionId: currentAgent.thinkingOptionId,
|
||||
}
|
||||
: null;
|
||||
@@ -782,6 +976,15 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
|
||||
});
|
||||
}}
|
||||
features={agent.features}
|
||||
onSetFeature={(featureId, value) => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
void client.setAgentFeature(agentId, featureId, value).catch((error) => {
|
||||
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
||||
});
|
||||
}}
|
||||
isModelLoading={isProviderModelsQueryLoading(modelsQuery)}
|
||||
disabled={!client}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user