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

This commit is contained in:
Mohamed Boudra
2026-03-18 22:54:09 +07:00
parent 1323cb13c4
commit 35da664a09
6 changed files with 56 additions and 30 deletions
@@ -39,6 +39,7 @@ const MODE_COLOR_MAP: Record<string, string> = {
safe: baseColors.green[500], safe: baseColors.green[500],
moderate: baseColors.amber[500], moderate: baseColors.amber[500],
dangerous: baseColors.red[500], dangerous: baseColors.red[500],
readonly: baseColors.purple[500],
}; };
type DropdownTriggerRenderProps = { type DropdownTriggerRenderProps = {
@@ -720,8 +720,9 @@ export function AgentInputArea({
disabled={!isConnected || voice?.isVoiceSwitching} disabled={!isConnected || voice?.isVoiceSwitching}
accessibilityLabel="Enable Voice mode" accessibilityLabel="Enable Voice mode"
accessibilityRole="button" accessibilityRole="button"
style={[ style={({ hovered }) => [
styles.realtimeVoiceButton as any, styles.realtimeVoiceButton as any,
(hovered ? styles.iconButtonHovered : undefined) as any,
(!isConnected || voice?.isVoiceSwitching ? styles.buttonDisabled : undefined) as any, (!isConnected || voice?.isVoiceSwitching ? styles.buttonDisabled : undefined) as any,
]} ]}
> >
@@ -901,9 +902,6 @@ const styles = StyleSheet.create(((theme: Theme) => ({
width: 28, width: 28,
height: 28, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.surface0,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -911,6 +909,9 @@ const styles = StyleSheet.create(((theme: Theme) => ({
backgroundColor: theme.colors.palette.green[600], backgroundColor: theme.colors.palette.green[600],
borderColor: theme.colors.palette.green[800], borderColor: theme.colors.palette.green[800],
}, },
iconButtonHovered: {
backgroundColor: theme.colors.surface2,
},
tooltipRow: { tooltipRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -2,6 +2,7 @@ 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 { import {
Bot,
Brain, Brain,
ChevronDown, ChevronDown,
ShieldAlert, ShieldAlert,
@@ -9,6 +10,8 @@ import {
ShieldOff, ShieldOff,
SlidersHorizontal, SlidersHorizontal,
} from 'lucide-react-native' } from 'lucide-react-native'
import { ClaudeIcon } from '@/components/icons/claude-icon'
import { CodexIcon } from '@/components/icons/codex-icon'
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 {
@@ -91,9 +94,18 @@ const MODE_ICONS = {
ShieldOff, ShieldOff,
} as const } as const
const PROVIDER_ICONS: Record<string, typeof Bot> = {
claude: ClaudeIcon as unknown as typeof Bot,
codex: CodexIcon as unknown as typeof Bot,
}
function getProviderIcon(provider: string): typeof Bot {
return PROVIDER_ICONS[provider] ?? Bot
}
function getModeIconColor( function getModeIconColor(
colorTier: AgentModeColorTier | undefined, colorTier: AgentModeColorTier | undefined,
palette: { green: { 500: string }; amber: { 500: string }; red: { 500: string } } palette: { green: { 500: string }; amber: { 500: string }; red: { 500: string }; purple: { 500: string } }
): string { ): string {
switch (colorTier) { switch (colorTier) {
case 'safe': case 'safe':
@@ -102,6 +114,8 @@ function getModeIconColor(
return palette.amber[500] return palette.amber[500]
case 'dangerous': case 'dangerous':
return palette.red[500] return palette.red[500]
case 'readonly':
return palette.purple[500]
default: default:
return palette.green[500] return palette.green[500]
} }
@@ -153,6 +167,7 @@ function ControlledStatusBar({
const modeVisuals = selectedModeId ? getModeVisuals(provider, selectedModeId) : undefined const modeVisuals = selectedModeId ? getModeVisuals(provider, selectedModeId) : undefined
const ModeIconComponent = modeVisuals?.icon ? MODE_ICONS[modeVisuals.icon] : null const ModeIconComponent = modeVisuals?.icon ? MODE_ICONS[modeVisuals.icon] : null
const modeIconColor = getModeIconColor(modeVisuals?.colorTier, theme.colors.palette) const modeIconColor = getModeIconColor(modeVisuals?.colorTier, theme.colors.palette)
const ProviderIcon = getProviderIcon(provider)
const hasAnyControl = const hasAnyControl =
Boolean(providerOptions?.length) || Boolean(providerOptions?.length) ||
@@ -210,7 +225,7 @@ function ControlledStatusBar({
) )
return ( return (
<View style={[styles.container, isWeb && { marginBottom: -theme.spacing[1] }]}> <View style={styles.container}>
{isWeb ? ( {isWeb ? (
<> <>
{providerOptions && providerOptions.length > 0 ? ( {providerOptions && providerOptions.length > 0 ? (
@@ -298,6 +313,7 @@ function ControlledStatusBar({
accessibilityLabel="Select agent model" accessibilityLabel="Select agent model"
testID="agent-model-selector" testID="agent-model-selector"
> >
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<Text style={styles.modeBadgeText}>{displayModel}</Text> <Text style={styles.modeBadgeText}>{displayModel}</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable> </Pressable>
@@ -326,14 +342,10 @@ function ControlledStatusBar({
(disabled || !canSelectThinking) && styles.disabledBadge, (disabled || !canSelectThinking) && styles.disabledBadge,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Select thinking option" accessibilityLabel={`Select thinking option (${displayThinking})`}
testID="agent-thinking-selector" testID="agent-thinking-selector"
> >
<Brain <Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
size={theme.iconSize.xs}
color={theme.colors.foregroundMuted}
style={{ marginTop: 1 }}
/>
<Text style={styles.modeBadgeText}>{displayThinking}</Text> <Text style={styles.modeBadgeText}>{displayThinking}</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable> </Pressable>
@@ -362,7 +374,7 @@ function ControlledStatusBar({
accessibilityLabel="Agent preferences" accessibilityLabel="Agent preferences"
testID="agent-preferences-button" testID="agent-preferences-button"
> >
<SlidersHorizontal size={theme.iconSize.lg} color={theme.colors.foreground} /> <SlidersHorizontal size={theme.iconSize.md} color={theme.colors.foreground} />
</Pressable> </Pressable>
<AdaptiveModalSheet <AdaptiveModalSheet
@@ -688,7 +700,7 @@ export function DraftAgentStatusBar({
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
container: { container: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'flex-end',
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
modeBadge: { modeBadge: {
@@ -701,10 +713,11 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius['2xl'], borderRadius: theme.borderRadius['2xl'],
}, },
modeIconBadge: { modeIconBadge: {
width: 28,
height: 28,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'transparent', backgroundColor: 'transparent',
padding: theme.spacing[1],
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
}, },
modeBadgeHovered: { modeBadgeHovered: {
@@ -722,8 +735,8 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
}, },
prefsButton: { prefsButton: {
width: 34, width: 28,
height: 34, height: 28,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
+16 -4
View File
@@ -944,7 +944,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
disabled={!isConnected || disabled} disabled={!isConnected || disabled}
accessibilityLabel="Attach images" accessibilityLabel="Attach images"
accessibilityRole="button" accessibilityRole="button"
style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]} style={({ hovered }) => [
styles.attachButton,
hovered && styles.iconButtonHovered,
(!isConnected || disabled) && styles.buttonDisabled,
]}
> >
<Paperclip size={theme.iconSize.md} color={theme.colors.foreground} /> <Paperclip size={theme.iconSize.md} color={theme.colors.foreground} />
</TooltipTrigger> </TooltipTrigger>
@@ -972,8 +976,9 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
? 'Stop dictation' ? 'Stop dictation'
: 'Start dictation' : 'Start dictation'
} }
style={[ style={({ hovered }) => [
styles.voiceButton, styles.voiceButton,
hovered && !isDictating && styles.iconButtonHovered,
(!isDictationStartEnabled) && styles.buttonDisabled, (!isDictationStartEnabled) && styles.buttonDisabled,
isDictating && styles.voiceButtonRecording, isDictating && styles.voiceButtonRecording,
]} ]}
@@ -1010,7 +1015,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
disabled={!isConnected || disabled} disabled={!isConnected || disabled}
accessibilityLabel="Queue message" accessibilityLabel="Queue message"
accessibilityRole="button" accessibilityRole="button"
style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]} style={({ hovered }) => [
styles.queueButton,
hovered && styles.iconButtonHovered,
(!isConnected || disabled) && styles.buttonDisabled,
]}
> >
<Plus size={theme.iconSize.md} color="white" /> <Plus size={theme.iconSize.md} color="white" />
</TooltipTrigger> </TooltipTrigger>
@@ -1174,7 +1183,7 @@ const styles = StyleSheet.create(((theme: any) => ({
}, },
leftButtonGroup: { leftButtonGroup: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'flex-end',
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
rightButtonGroup: { rightButtonGroup: {
@@ -1215,6 +1224,9 @@ const styles = StyleSheet.create(((theme: any) => ({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
iconButtonHovered: {
backgroundColor: theme.colors.surface2,
},
tooltipRow: { tooltipRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -145,9 +145,8 @@ function TabChip({
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.tab, styles.tab,
Platform.OS === "web" && Platform.OS === "web" &&
({ isDragging &&
cursor: isDragging ? "grabbing" : "grab", ({ cursor: "grabbing" } as const),
} as const),
{ {
minWidth: resolvedTabWidth, minWidth: resolvedTabWidth,
width: resolvedTabWidth, width: resolvedTabWidth,
@@ -1,7 +1,7 @@
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 AgentModeColorTier = 'safe' | 'moderate' | 'dangerous' | 'readonly';
export type AgentModeIcon = 'ShieldCheck' | 'ShieldAlert' | 'ShieldOff'; export type AgentModeIcon = 'ShieldCheck' | 'ShieldAlert' | 'ShieldOff';
export interface AgentModeVisuals { export interface AgentModeVisuals {
@@ -27,19 +27,19 @@ export interface AgentProviderDefinition {
const CLAUDE_MODES: AgentProviderModeDefinition[] = [ 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", label: "Always Ask", description: "Prompts for permission the first time a tool is used", icon: "ShieldCheck", colorTier: "safe" },
{ id: "acceptEdits", label: "Accept File Edits", description: "Automatically approves edit-focused tools without prompting", icon: "ShieldAlert", colorTier: "moderate" }, { id: "acceptEdits", label: "Accept File Edits", description: "Automatically approves edit-focused tools without prompting", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "plan", label: "Plan Mode", description: "Analyze the codebase without executing tools or edits", icon: "ShieldCheck", colorTier: "safe" }, { id: "plan", label: "Plan Mode", description: "Analyze the codebase without executing tools or edits", icon: "ShieldCheck", colorTier: "readonly" },
{ id: "bypassPermissions", label: "Bypass", description: "Skip all permission prompts (use with caution)", icon: "ShieldOff", colorTier: "dangerous" }, { id: "bypassPermissions", label: "Bypass", description: "Skip all permission prompts (use with caution)", icon: "ShieldAlert", colorTier: "dangerous" },
]; ];
const CODEX_MODES: AgentProviderModeDefinition[] = [ 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", label: "Read Only", description: "Read files and answer questions. Manual approval required for edits, commands, or network ops.", icon: "ShieldCheck", colorTier: "readonly" },
{ id: "auto", label: "Auto", description: "Edit files and run commands but still request approval before escalating scope.", icon: "ShieldAlert", colorTier: "moderate" }, { id: "auto", label: "Auto", description: "Edit files and run commands but still request approval before escalating scope.", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "full-access", label: "Full Access", description: "Edit files, run commands, and access the network without additional prompts.", icon: "ShieldOff", colorTier: "dangerous" }, { id: "full-access", label: "Full Access", description: "Edit files, run commands, and access the network without additional prompts.", icon: "ShieldAlert", colorTier: "dangerous" },
]; ];
const OPENCODE_MODES: AgentProviderModeDefinition[] = [ const OPENCODE_MODES: AgentProviderModeDefinition[] = [
{ id: "build", label: "Build", description: "Allows edits and tool execution for implementation work", icon: "ShieldAlert", colorTier: "moderate" }, { id: "build", label: "Build", description: "Allows edits and tool execution for implementation work", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "plan", label: "Plan", description: "Read-only planning mode that avoids file edits", icon: "ShieldCheck", colorTier: "safe" }, { id: "plan", label: "Plan", description: "Read-only planning mode that avoids file edits", icon: "ShieldCheck", colorTier: "readonly" },
]; ];
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [