Add agent mode visuals (icons and color tiers) to UI components
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user