From 35da664a099980b5f105d1ecf73cb66df95b7760 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 18 Mar 2026 22:54:09 +0700 Subject: [PATCH] Add agent mode visuals (icons and color tiers) to UI components --- .../agent-form/agent-form-dropdowns.tsx | 1 + .../app/src/components/agent-input-area.tsx | 9 +++-- .../app/src/components/agent-status-bar.tsx | 39 ++++++++++++------- packages/app/src/components/message-input.tsx | 20 ++++++++-- .../workspace/workspace-desktop-tabs-row.tsx | 5 +-- .../src/server/agent/provider-manifest.ts | 12 +++--- 6 files changed, 56 insertions(+), 30 deletions(-) diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 3c754cbb2..58102e6ac 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -39,6 +39,7 @@ const MODE_COLOR_MAP: Record = { safe: baseColors.green[500], moderate: baseColors.amber[500], dangerous: baseColors.red[500], + readonly: baseColors.purple[500], }; type DropdownTriggerRenderProps = { diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 71f38fe24..7ccc059c9 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -720,8 +720,9 @@ export function AgentInputArea({ disabled={!isConnected || voice?.isVoiceSwitching} accessibilityLabel="Enable Voice mode" accessibilityRole="button" - style={[ + style={({ hovered }) => [ styles.realtimeVoiceButton as any, + (hovered ? styles.iconButtonHovered : undefined) as any, (!isConnected || voice?.isVoiceSwitching ? styles.buttonDisabled : undefined) as any, ]} > @@ -901,9 +902,6 @@ const styles = StyleSheet.create(((theme: Theme) => ({ width: 28, height: 28, borderRadius: theme.borderRadius.full, - backgroundColor: theme.colors.surface0, - borderWidth: theme.borderWidth[1], - borderColor: theme.colors.border, alignItems: 'center', justifyContent: 'center', }, @@ -911,6 +909,9 @@ const styles = StyleSheet.create(((theme: Theme) => ({ backgroundColor: theme.colors.palette.green[600], borderColor: theme.colors.palette.green[800], }, + iconButtonHovered: { + backgroundColor: theme.colors.surface2, + }, tooltipRow: { flexDirection: 'row', alignItems: 'center', diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 7fdd57111..415ebfce3 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { View, Text, Platform, Pressable } from 'react-native' import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { + Bot, Brain, ChevronDown, ShieldAlert, @@ -9,6 +10,8 @@ import { ShieldOff, SlidersHorizontal, } 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 { useSessionStore } from '@/stores/session-store' import { @@ -91,9 +94,18 @@ const MODE_ICONS = { ShieldOff, } as const +const PROVIDER_ICONS: Record = { + 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( 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 { switch (colorTier) { case 'safe': @@ -102,6 +114,8 @@ function getModeIconColor( return palette.amber[500] case 'dangerous': return palette.red[500] + case 'readonly': + return palette.purple[500] default: return palette.green[500] } @@ -153,6 +167,7 @@ function ControlledStatusBar({ 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 ProviderIcon = getProviderIcon(provider) const hasAnyControl = Boolean(providerOptions?.length) || @@ -210,7 +225,7 @@ function ControlledStatusBar({ ) return ( - + {isWeb ? ( <> {providerOptions && providerOptions.length > 0 ? ( @@ -298,6 +313,7 @@ function ControlledStatusBar({ accessibilityLabel="Select agent model" testID="agent-model-selector" > + {displayModel} @@ -326,14 +342,10 @@ function ControlledStatusBar({ (disabled || !canSelectThinking) && styles.disabledBadge, ]} accessibilityRole="button" - accessibilityLabel="Select thinking option" + accessibilityLabel={`Select thinking option (${displayThinking})`} testID="agent-thinking-selector" > - + {displayThinking} @@ -362,7 +374,7 @@ function ControlledStatusBar({ accessibilityLabel="Agent preferences" testID="agent-preferences-button" > - + ({ container: { flexDirection: 'row', - alignItems: 'center', + alignItems: 'flex-end', gap: theme.spacing[1], }, modeBadge: { @@ -701,10 +713,11 @@ const styles = StyleSheet.create((theme) => ({ borderRadius: theme.borderRadius['2xl'], }, modeIconBadge: { + width: 28, + height: 28, alignItems: 'center', justifyContent: 'center', backgroundColor: 'transparent', - padding: theme.spacing[1], borderRadius: theme.borderRadius.full, }, modeBadgeHovered: { @@ -722,8 +735,8 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.normal, }, prefsButton: { - width: 34, - height: 34, + width: 28, + height: 28, borderRadius: theme.borderRadius.full, alignItems: 'center', justifyContent: 'center', diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 70a636456..32a673f79 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -944,7 +944,11 @@ export const MessageInput = forwardRef(funct disabled={!isConnected || disabled} accessibilityLabel="Attach images" accessibilityRole="button" - style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]} + style={({ hovered }) => [ + styles.attachButton, + hovered && styles.iconButtonHovered, + (!isConnected || disabled) && styles.buttonDisabled, + ]} > @@ -972,8 +976,9 @@ export const MessageInput = forwardRef(funct ? 'Stop dictation' : 'Start dictation' } - style={[ + style={({ hovered }) => [ styles.voiceButton, + hovered && !isDictating && styles.iconButtonHovered, (!isDictationStartEnabled) && styles.buttonDisabled, isDictating && styles.voiceButtonRecording, ]} @@ -1010,7 +1015,11 @@ export const MessageInput = forwardRef(funct disabled={!isConnected || disabled} accessibilityLabel="Queue message" accessibilityRole="button" - style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]} + style={({ hovered }) => [ + styles.queueButton, + hovered && styles.iconButtonHovered, + (!isConnected || disabled) && styles.buttonDisabled, + ]} > @@ -1174,7 +1183,7 @@ const styles = StyleSheet.create(((theme: any) => ({ }, leftButtonGroup: { flexDirection: 'row', - alignItems: 'center', + alignItems: 'flex-end', gap: theme.spacing[2], }, rightButtonGroup: { @@ -1215,6 +1224,9 @@ const styles = StyleSheet.create(((theme: any) => ({ alignItems: 'center', justifyContent: 'center', }, + iconButtonHovered: { + backgroundColor: theme.colors.surface2, + }, tooltipRow: { flexDirection: 'row', alignItems: 'center', diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 5b15908ba..f06c42320 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -145,9 +145,8 @@ function TabChip({ style={({ hovered, pressed }) => [ styles.tab, Platform.OS === "web" && - ({ - cursor: isDragging ? "grabbing" : "grab", - } as const), + isDragging && + ({ cursor: "grabbing" } as const), { minWidth: resolvedTabWidth, width: resolvedTabWidth, diff --git a/packages/server/src/server/agent/provider-manifest.ts b/packages/server/src/server/agent/provider-manifest.ts index b9ceab433..3eb950c7e 100644 --- a/packages/server/src/server/agent/provider-manifest.ts +++ b/packages/server/src/server/agent/provider-manifest.ts @@ -1,7 +1,7 @@ import { z } from "zod"; 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 interface AgentModeVisuals { @@ -27,19 +27,19 @@ export interface AgentProviderDefinition { 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: "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: "bypassPermissions", label: "Bypass", description: "Skip all permission prompts (use with caution)", icon: "ShieldOff", colorTier: "dangerous" }, + { 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: "ShieldAlert", colorTier: "dangerous" }, ]; 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: "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[] = [ { 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[] = [