Add agent mode visuals (icons and color tiers) to UI components

This commit is contained in:
Mohamed Boudra
2026-03-18 22:31:31 +07:00
parent 3287f2d00b
commit 1323cb13c4
9 changed files with 190 additions and 104 deletions
@@ -17,7 +17,7 @@ import {
BottomSheetBackgroundProps, BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import Animated from "react-native-reanimated"; import Animated from "react-native-reanimated";
import { ChevronDown, ChevronRight, Pencil, Check, X, Bot, Brain, Shield } from "lucide-react-native"; import { ChevronDown, ChevronRight, Pencil, Check, X, Bot, Brain, ShieldCheck, ShieldAlert, ShieldOff } from "lucide-react-native";
import { theme as defaultTheme } from "@/styles/theme"; import { theme as defaultTheme } from "@/styles/theme";
import type { import type {
AgentMode, AgentMode,
@@ -25,7 +25,21 @@ import type {
AgentProvider, AgentProvider,
} from "@server/server/agent/agent-sdk-types"; } from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest"; import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { getModeVisuals, type AgentModeIcon } from "@server/server/agent/provider-manifest";
import { Combobox, ComboboxItem, ComboboxEmpty } from "@/components/ui/combobox"; import { Combobox, ComboboxItem, ComboboxEmpty } from "@/components/ui/combobox";
import { baseColors } from "@/styles/theme";
const MODE_ICON_MAP: Record<AgentModeIcon, typeof ShieldCheck> = {
ShieldCheck,
ShieldAlert,
ShieldOff,
};
const MODE_COLOR_MAP: Record<string, string> = {
safe: baseColors.green[500],
moderate: baseColors.amber[500],
dangerous: baseColors.red[500],
};
type DropdownTriggerRenderProps = { type DropdownTriggerRenderProps = {
label: string; label: string;
@@ -563,6 +577,10 @@ export function AgentConfigRow({
const effectiveSelectedThinkingOption = const effectiveSelectedThinkingOption =
selectedThinkingOptionId || thinkingSelectOptions[0]?.id || ""; selectedThinkingOptionId || thinkingSelectOptions[0]?.id || "";
const selectedModeVisuals = getModeVisuals(selectedProvider, effectiveSelectedMode);
const ModeIcon = MODE_ICON_MAP[selectedModeVisuals?.icon ?? "ShieldCheck"];
const modeIconColor = MODE_COLOR_MAP[selectedModeVisuals?.colorTier ?? "safe"];
return ( return (
<View style={styles.agentConfigRow}> <View style={styles.agentConfigRow}>
<View style={styles.agentConfigColumn}> <View style={styles.agentConfigColumn}>
@@ -603,7 +621,7 @@ export function AgentConfigRow({
placeholder="Default" placeholder="Default"
disabled={disabled || modeOptions.length === 0} disabled={disabled || modeOptions.length === 0}
onSelect={onSelectMode} onSelect={onSelectMode}
icon={<Shield size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />} icon={<ModeIcon size={defaultTheme.iconSize.md} color={modeIconColor} />}
showLabel={false} showLabel={false}
testID="draft-mode-select" testID="draft-mode-select"
/> />
@@ -699,7 +699,7 @@ export function AgentInputArea({
{isCancellingAgent ? ( {isCancellingAgent ? (
<ActivityIndicator size="small" color="white" /> <ActivityIndicator size="small" color="white" />
) : ( ) : (
<Square size={theme.iconSize.lg} color="white" fill="white" /> <Square size={theme.iconSize.md} color="white" fill="white" />
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
@@ -728,7 +728,7 @@ export function AgentInputArea({
{voice?.isVoiceSwitching ? ( {voice?.isVoiceSwitching ? (
<ActivityIndicator size="small" color="white" /> <ActivityIndicator size="small" color="white" />
) : ( ) : (
<AudioLines size={theme.iconSize.lg} color={theme.colors.foreground} /> <AudioLines size={theme.iconSize.md} color={theme.colors.foreground} />
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
@@ -885,8 +885,8 @@ const styles = StyleSheet.create(((theme: Theme) => ({
zIndex: 30, zIndex: 30,
}, },
cancelButton: { cancelButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.palette.red[600], backgroundColor: theme.colors.palette.red[600],
alignItems: 'center', alignItems: 'center',
@@ -898,8 +898,8 @@ const styles = StyleSheet.create(((theme: Theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
realtimeVoiceButton: { realtimeVoiceButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.surface0, backgroundColor: theme.colors.surface0,
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
@@ -1,7 +1,14 @@
import { useCallback, useMemo, useRef, useState } from 'react' import { useCallback, useMemo, useRef, useState } from 'react'
import { View, Text, Platform, Pressable } from 'react-native' import { View, Text, Platform, Pressable } from 'react-native'
import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { StyleSheet, useUnistyles } from 'react-native-unistyles'
import { Brain, ChevronDown, SlidersHorizontal } from 'lucide-react-native' import {
Brain,
ChevronDown,
ShieldAlert,
ShieldCheck,
ShieldOff,
SlidersHorizontal,
} from 'lucide-react-native'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useSessionStore } from '@/stores/session-store' import { useSessionStore } from '@/stores/session-store'
import { import {
@@ -10,7 +17,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Combobox, type ComboboxOption } from '@/components/ui/combobox' import { Combobox, ComboboxItem, type ComboboxOption } from '@/components/ui/combobox'
import { AdaptiveModalSheet } from '@/components/adaptive-modal-sheet' import { AdaptiveModalSheet } from '@/components/adaptive-modal-sheet'
import type { import type {
AgentMode, AgentMode,
@@ -18,6 +25,11 @@ import type {
AgentProvider, AgentProvider,
} from '@server/server/agent/agent-sdk-types' } from '@server/server/agent/agent-sdk-types'
import type { AgentProviderDefinition } from '@server/server/agent/provider-manifest' import type { AgentProviderDefinition } from '@server/server/agent/provider-manifest'
import {
getModeVisuals,
type AgentModeColorTier,
type AgentModeIcon,
} from '@server/server/agent/provider-manifest'
import { normalizeModelId, resolveAgentModelSelection } from '@/components/agent-status-bar.utils' import { normalizeModelId, resolveAgentModelSelection } from '@/components/agent-status-bar.utils'
type StatusOption = { type StatusOption = {
@@ -26,6 +38,7 @@ type StatusOption = {
} }
type ControlledAgentStatusBarProps = { type ControlledAgentStatusBarProps = {
provider: string
providerOptions?: StatusOption[] providerOptions?: StatusOption[]
selectedProviderId?: string selectedProviderId?: string
onSelectProvider?: (providerId: string) => void onSelectProvider?: (providerId: string) => void
@@ -72,7 +85,31 @@ function findOptionLabel(options: StatusOption[] | undefined, selectedId: string
return selected?.label ?? fallback return selected?.label ?? fallback
} }
const MODE_ICONS = {
ShieldCheck,
ShieldAlert,
ShieldOff,
} as const
function getModeIconColor(
colorTier: AgentModeColorTier | undefined,
palette: { green: { 500: string }; amber: { 500: string }; red: { 500: string } }
): string {
switch (colorTier) {
case 'safe':
return palette.green[500]
case 'moderate':
return palette.amber[500]
case 'dangerous':
return palette.red[500]
default:
return palette.green[500]
}
}
function ControlledStatusBar({ function ControlledStatusBar({
provider,
providerOptions, providerOptions,
selectedProviderId, selectedProviderId,
onSelectProvider, onSelectProvider,
@@ -113,6 +150,10 @@ function ControlledStatusBar({
: findOptionLabel(modelOptions, selectedModelId, 'Auto') : findOptionLabel(modelOptions, selectedModelId, 'Auto')
const displayThinking = findOptionLabel(thinkingOptions, selectedThinkingOptionId, 'auto') const displayThinking = findOptionLabel(thinkingOptions, selectedThinkingOptionId, 'auto')
const modeVisuals = selectedModeId ? getModeVisuals(provider, selectedModeId) : undefined
const ModeIconComponent = modeVisuals?.icon ? MODE_ICONS[modeVisuals.icon] : null
const modeIconColor = getModeIconColor(modeVisuals?.colorTier, theme.colors.palette)
const hasAnyControl = const hasAnyControl =
Boolean(providerOptions?.length) || Boolean(providerOptions?.length) ||
Boolean(modeOptions?.length) || Boolean(modeOptions?.length) ||
@@ -144,6 +185,23 @@ function ControlledStatusBar({
[thinkingOptions] [thinkingOptions]
) )
const renderModeOption = useCallback(
({ option, selected, active, onPress }: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void }) => {
const visuals = getModeVisuals(provider, option.id)
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck
return (
<ComboboxItem
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={<IconComponent size={16} color={theme.colors.foreground} />}
/>
)
},
[provider, theme.colors.foreground]
)
const handleOpenChange = useCallback( const handleOpenChange = useCallback(
(selector: 'provider' | 'mode' | 'model' | 'thinking') => (nextOpen: boolean) => { (selector: 'provider' | 'mode' | 'model' | 'thinking') => (nextOpen: boolean) => {
setOpenSelector(nextOpen ? selector : null) setOpenSelector(nextOpen ? selector : null)
@@ -196,17 +254,20 @@ function ControlledStatusBar({
disabled={disabled || !canSelectMode} disabled={disabled || !canSelectMode}
onPress={() => setOpenSelector(openSelector === 'mode' ? null : 'mode')} onPress={() => setOpenSelector(openSelector === 'mode' ? null : 'mode')}
style={({ pressed, hovered }) => [ style={({ pressed, hovered }) => [
styles.modeBadge, styles.modeIconBadge,
hovered && styles.modeBadgeHovered, hovered && styles.modeBadgeHovered,
(pressed || openSelector === 'mode') && styles.modeBadgePressed, (pressed || openSelector === 'mode') && styles.modeBadgePressed,
(disabled || !canSelectMode) && styles.disabledBadge, (disabled || !canSelectMode) && styles.disabledBadge,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Select agent mode" accessibilityLabel={`Select agent mode (${displayMode})`}
testID="agent-mode-selector" testID="agent-mode-selector"
> >
<Text style={styles.modeBadgeText}>{displayMode}</Text> {ModeIconComponent ? (
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
) : (
<ShieldCheck size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
)}
</Pressable> </Pressable>
<Combobox <Combobox
options={comboboxModeOptions} options={comboboxModeOptions}
@@ -217,6 +278,7 @@ function ControlledStatusBar({
onOpenChange={handleOpenChange('mode')} onOpenChange={handleOpenChange('mode')}
anchorRef={modeAnchorRef} anchorRef={modeAnchorRef}
desktopPlacement="top-start" desktopPlacement="top-start"
renderOption={renderModeOption}
/> />
</> </>
) : null} ) : null}
@@ -355,19 +417,27 @@ function ControlledStatusBar({
accessibilityLabel="Select agent mode" accessibilityLabel="Select agent mode"
testID="agent-preferences-mode" testID="agent-preferences-mode"
> >
{ModeIconComponent ? (
<ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
) : null}
<Text style={styles.sheetSelectText}>{displayMode}</Text> <Text style={styles.sheetSelectText}>{displayMode}</Text>
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} /> <ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start"> <DropdownMenuContent side="top" align="start">
{modeOptions.map((mode) => ( {modeOptions.map((mode) => {
<DropdownMenuItem const visuals = getModeVisuals(provider, mode.id)
key={mode.id} const Icon = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck
selected={mode.id === selectedModeId} return (
onSelect={() => onSelectMode?.(mode.id)} <DropdownMenuItem
> key={mode.id}
{mode.label} selected={mode.id === selectedModeId}
</DropdownMenuItem> onSelect={() => onSelectMode?.(mode.id)}
))} leading={<Icon size={16} color={theme.colors.foreground} />}
>
{mode.label}
</DropdownMenuItem>
)
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</View> </View>
@@ -504,6 +574,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
return ( return (
<ControlledStatusBar <ControlledStatusBar
provider={agent.provider}
modeOptions={ modeOptions={
modeOptions.length > 0 modeOptions.length > 0
? modeOptions ? modeOptions
@@ -571,7 +642,10 @@ export function DraftAgentStatusBar({
if (modeOptions.length === 0) { if (modeOptions.length === 0) {
return [{ id: '', label: 'Default' }] return [{ id: '', label: 'Default' }]
} }
return modeOptions.map((mode) => ({ id: mode.id, label: mode.label })) return modeOptions.map((mode) => ({
id: mode.id,
label: mode.label,
}))
}, [modeOptions]) }, [modeOptions])
const modelOptions = useMemo<StatusOption[]>(() => { const modelOptions = useMemo<StatusOption[]>(() => {
@@ -592,6 +666,7 @@ export function DraftAgentStatusBar({
return ( return (
<ControlledStatusBar <ControlledStatusBar
provider={selectedProvider}
providerOptions={providerOptions} providerOptions={providerOptions}
selectedProviderId={selectedProvider} selectedProviderId={selectedProvider}
onSelectProvider={(providerId) => onSelectProvider(providerId as AgentProvider)} onSelectProvider={(providerId) => onSelectProvider(providerId as AgentProvider)}
@@ -625,6 +700,13 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[1], paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius['2xl'], borderRadius: theme.borderRadius['2xl'],
}, },
modeIconBadge: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
padding: theme.spacing[1],
borderRadius: theme.borderRadius.full,
},
modeBadgeHovered: { modeBadgeHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
+14 -14
View File
@@ -946,7 +946,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
accessibilityRole="button" accessibilityRole="button"
style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]} style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]}
> >
<Paperclip size={theme.iconSize.lg} color={theme.colors.foreground} /> <Paperclip size={theme.iconSize.md} color={theme.colors.foreground} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>Attach images</Text> <Text style={styles.tooltipText}>Attach images</Text>
@@ -979,11 +979,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
]} ]}
> >
{isDictating ? ( {isDictating ? (
<Square size={theme.iconSize.lg} color="white" fill="white" /> <Square size={theme.iconSize.md} color="white" fill="white" />
) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? ( ) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? (
<MicOff size={theme.iconSize.lg} color={theme.colors.foreground} /> <MicOff size={theme.iconSize.md} color={theme.colors.foreground} />
) : ( ) : (
<Mic size={theme.iconSize.lg} color={theme.colors.foreground} /> <Mic size={theme.iconSize.md} color={theme.colors.foreground} />
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
@@ -1012,7 +1012,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
accessibilityRole="button" accessibilityRole="button"
style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]} style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]}
> >
<Plus size={theme.iconSize.lg} color="white" /> <Plus size={theme.iconSize.md} color="white" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
<View style={styles.tooltipRow}> <View style={styles.tooltipRow}>
@@ -1034,7 +1034,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
{isSubmitLoading ? ( {isSubmitLoading ? (
<ActivityIndicator size="small" color="white" /> <ActivityIndicator size="small" color="white" />
) : ( ) : (
<ArrowUp size={theme.iconSize.lg} color="white" /> <ArrowUp size={theme.iconSize.md} color="white" />
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}> <TooltipContent side="top" align="center" offset={8}>
@@ -1183,15 +1183,15 @@ const styles = StyleSheet.create(((theme: any) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
attachButton: { attachButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
voiceButton: { voiceButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -1200,16 +1200,16 @@ const styles = StyleSheet.create(((theme: any) => ({
backgroundColor: theme.colors.destructive, backgroundColor: theme.colors.destructive,
}, },
queueButton: { queueButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surface1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
sendButton: { sendButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.accent, backgroundColor: theme.colors.accent,
alignItems: 'center', alignItems: 'center',
+16 -9
View File
@@ -144,6 +144,7 @@ export interface ComboboxItemProps {
label: string label: string
description?: string description?: string
kind?: 'directory' | 'file' kind?: 'directory' | 'file'
leadingSlot?: ReactNode
selected?: boolean selected?: boolean
active?: boolean active?: boolean
onPress: () => void onPress: () => void
@@ -154,12 +155,26 @@ export function ComboboxItem({
label, label,
description, description,
kind, kind,
leadingSlot,
selected, selected,
active, active,
onPress, onPress,
testID, testID,
}: ComboboxItemProps): ReactElement { }: ComboboxItemProps): ReactElement {
const { theme } = useUnistyles() const { theme } = useUnistyles()
const leadingContent = leadingSlot ? (
<View style={styles.comboboxItemLeadingSlot}>{leadingSlot}</View>
) : kind === 'directory' || kind === 'file' ? (
<View style={styles.comboboxItemLeadingSlot}>
{kind === 'directory' ? (
<Folder size={16} color={theme.colors.foregroundMuted} />
) : (
<File size={16} color={theme.colors.foregroundMuted} />
)}
</View>
) : null
return ( return (
<Pressable <Pressable
testID={testID} testID={testID}
@@ -171,15 +186,7 @@ export function ComboboxItem({
active && styles.comboboxItemActive, active && styles.comboboxItemActive,
]} ]}
> >
{kind === 'directory' || kind === 'file' ? ( {leadingContent}
<View style={styles.comboboxItemLeadingSlot}>
{kind === 'directory' ? (
<Folder size={16} color={theme.colors.foregroundMuted} />
) : (
<File size={16} color={theme.colors.foregroundMuted} />
)}
</View>
) : null}
<View style={styles.comboboxItemContent}> <View style={styles.comboboxItemContent}>
<Text numberOfLines={1} style={styles.comboboxItemLabel}> <Text numberOfLines={1} style={styles.comboboxItemLabel}>
{label} {label}
+2 -2
View File
@@ -1,4 +1,4 @@
import { Pencil } from "lucide-react-native"; import { SquarePen } from "lucide-react-native";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab"; import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
import { usePaneContext } from "@/panels/pane-context"; import { usePaneContext } from "@/panels/pane-context";
@@ -11,7 +11,7 @@ function useDraftPanelDescriptor() {
label: "New Agent", label: "New Agent",
subtitle: "New Agent", subtitle: "New Agent",
titleState: "ready" as const, titleState: "ready" as const,
icon: Pencil, icon: SquarePen,
statusBucket: null, statusBucket: null,
}; };
} }
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react"; import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react";
import { ActivityIndicator, Platform, Pressable, ScrollView, Text, View, type LayoutChangeEvent } from "react-native"; import { ActivityIndicator, Platform, Pressable, ScrollView, Text, View, type LayoutChangeEvent } from "react-native";
import { Columns2, Plus, Rows2, SquareTerminal, X } from "lucide-react-native"; import { Columns2, Rows2, SquarePen, SquareTerminal, X } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { SortableInlineList } from "@/components/sortable-inline-list"; import { SortableInlineList } from "@/components/sortable-inline-list";
import { import {
@@ -438,7 +438,7 @@ export function WorkspaceDesktopTabsRow({
(hovered || pressed) && styles.newTabActionButtonHovered, (hovered || pressed) && styles.newTabActionButtonHovered,
]} ]}
> >
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <SquarePen size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" align="end" offset={8}> <TooltipContent side="bottom" align="end" offset={8}>
<View style={styles.newTabTooltipRow}> <View style={styles.newTabTooltipRow}>
@@ -17,7 +17,7 @@ import {
Ellipsis, Ellipsis,
EllipsisVertical, EllipsisVertical,
PanelRight, PanelRight,
Plus, SquarePen,
SquareTerminal, SquareTerminal,
} from "lucide-react-native"; } from "lucide-react-native";
import { GestureDetector } from "react-native-gesture-handler"; import { GestureDetector } from "react-native-gesture-handler";
@@ -422,7 +422,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
(hovered || pressed) && styles.newTabActionButtonHovered, (hovered || pressed) && styles.newTabActionButtonHovered,
]} ]}
> >
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <SquarePen size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" align="end" offset={8}> <TooltipContent side="bottom" align="end" offset={8}>
<View style={styles.newTabTooltipRow}> <View style={styles.newTabTooltipRow}>
@@ -1,12 +1,22 @@
import { z } from "zod"; import { z } from "zod";
import type { AgentMode } from "./agent-sdk-types.js"; import type { AgentMode } from "./agent-sdk-types.js";
export type AgentModeColorTier = 'safe' | 'moderate' | 'dangerous';
export type AgentModeIcon = 'ShieldCheck' | 'ShieldAlert' | 'ShieldOff';
export interface AgentModeVisuals {
icon: AgentModeIcon;
colorTier: AgentModeColorTier;
}
export interface AgentProviderModeDefinition extends AgentMode, AgentModeVisuals {}
export interface AgentProviderDefinition { export interface AgentProviderDefinition {
id: string; id: string;
label: string; label: string;
description: string; description: string;
defaultModeId: string | null; defaultModeId: string | null;
modes: AgentMode[]; modes: AgentProviderModeDefinition[];
voice?: { voice?: {
enabled: boolean; enabled: boolean;
defaultModeId: string; defaultModeId: string;
@@ -14,60 +24,22 @@ export interface AgentProviderDefinition {
}; };
} }
const CLAUDE_MODES: AgentMode[] = [ const CLAUDE_MODES: AgentProviderModeDefinition[] = [
{ { id: "default", label: "Always Ask", description: "Prompts for permission the first time a tool is used", icon: "ShieldCheck", colorTier: "safe" },
id: "default", { id: "acceptEdits", label: "Accept File Edits", description: "Automatically approves edit-focused tools without prompting", icon: "ShieldAlert", colorTier: "moderate" },
label: "Always Ask", { id: "plan", label: "Plan Mode", description: "Analyze the codebase without executing tools or edits", icon: "ShieldCheck", colorTier: "safe" },
description: "Prompts for permission the first time a tool is used", { id: "bypassPermissions", label: "Bypass", description: "Skip all permission prompts (use with caution)", icon: "ShieldOff", colorTier: "dangerous" },
},
{
id: "acceptEdits",
label: "Accept File Edits",
description: "Automatically approves edit-focused tools without prompting",
},
{
id: "plan",
label: "Plan Mode",
description: "Analyze the codebase without executing tools or edits",
},
{
id: "bypassPermissions",
label: "Bypass",
description: "Skip all permission prompts (use with caution)",
},
]; ];
const CODEX_MODES: AgentMode[] = [ const CODEX_MODES: AgentProviderModeDefinition[] = [
{ { id: "read-only", label: "Read Only", description: "Read files and answer questions. Manual approval required for edits, commands, or network ops.", icon: "ShieldCheck", colorTier: "safe" },
id: "read-only", { id: "auto", label: "Auto", description: "Edit files and run commands but still request approval before escalating scope.", icon: "ShieldAlert", colorTier: "moderate" },
label: "Read Only", { id: "full-access", label: "Full Access", description: "Edit files, run commands, and access the network without additional prompts.", icon: "ShieldOff", colorTier: "dangerous" },
description:
"Read files and answer questions. Manual approval required for edits, commands, or network ops.",
},
{
id: "auto",
label: "Auto",
description:
"Edit files and run commands but still request approval before escalating scope.",
},
{
id: "full-access",
label: "Full Access",
description: "Edit files, run commands, and access the network without additional prompts.",
},
]; ];
const OPENCODE_MODES: AgentMode[] = [ const OPENCODE_MODES: AgentProviderModeDefinition[] = [
{ { id: "build", label: "Build", description: "Allows edits and tool execution for implementation work", icon: "ShieldAlert", colorTier: "moderate" },
id: "build", { id: "plan", label: "Plan", description: "Read-only planning mode that avoids file edits", icon: "ShieldCheck", colorTier: "safe" },
label: "Build",
description: "Allows edits and tool execution for implementation work",
},
{
id: "plan",
label: "Plan",
description: "Read-only planning mode that avoids file edits",
},
]; ];
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
@@ -126,3 +98,10 @@ export const AgentProviderSchema = z.enum(AGENT_PROVIDER_IDS);
export function isValidAgentProvider(value: string): boolean { export function isValidAgentProvider(value: string): boolean {
return AGENT_PROVIDER_IDS.includes(value); return AGENT_PROVIDER_IDS.includes(value);
} }
export function getModeVisuals(provider: string, modeId: string): AgentModeVisuals | undefined {
const definition = AGENT_PROVIDER_DEFINITIONS.find((entry) => entry.id === provider);
const mode = definition?.modes.find((m) => m.id === modeId);
if (!mode) return undefined;
return { icon: mode.icon, colorTier: mode.colorTier };
}