Based on the changes, I can see this is a significant feature addition that adds the ability to create agents and communicate with them through both text and audio, with a modal UI for agent creation and mode switching.
``` feat: add agent creation modal and agent-specific messaging - Add CreateAgentModal component for creating new agents with mode selection - Implement agent-specific text and audio message routing - Add mode selector modal with visual mode switching UI - Introduce unified stream reducer pattern for agent activity - Refactor AgentStreamView to use StreamItem[] instead of raw notifications - Update ActiveProcesses to show orchestrator pill and support mode switching - Add message types: send_agent_message, send_agent_audio, create_agent_request, set_agent_mode - Implement agent audio transcription and message forwarding in session handler - Add mode badge UI showing current agent mode - Update tmux capture-pane to allowed commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> ```
This commit is contained in:
@@ -18,10 +18,10 @@ export interface ActiveProcessesProps {
|
||||
isDead: boolean;
|
||||
exitCode: number | null;
|
||||
}>;
|
||||
activeProcessId: string | null;
|
||||
activeProcessType: 'agent' | null;
|
||||
viewMode: 'orchestrator' | 'agent';
|
||||
activeAgentId: string | null;
|
||||
onSelectAgent: (id: string) => void;
|
||||
onBackToOrchestrator: () => void;
|
||||
onSelectOrchestrator: () => void;
|
||||
}
|
||||
|
||||
function getAgentStatusColor(status: AgentStatus): string {
|
||||
@@ -122,6 +122,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
processTextActive: {
|
||||
color: theme.colors.primaryForeground,
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
@@ -138,39 +141,43 @@ const styles = StyleSheet.create((theme) => ({
|
||||
export function ActiveProcesses({
|
||||
agents,
|
||||
commands,
|
||||
activeProcessId,
|
||||
activeProcessType,
|
||||
viewMode,
|
||||
activeAgentId,
|
||||
onSelectAgent,
|
||||
onBackToOrchestrator,
|
||||
onSelectOrchestrator,
|
||||
}: ActiveProcessesProps) {
|
||||
const hasActiveProcess = activeProcessId !== null && activeProcessType !== null;
|
||||
|
||||
if (agents.length === 0 && commands.length === 0) {
|
||||
// Only show if there's at least one agent
|
||||
if (agents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{hasActiveProcess && (
|
||||
<View style={styles.header}>
|
||||
<Pressable
|
||||
onPress={onBackToOrchestrator}
|
||||
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonActive]}
|
||||
>
|
||||
<Text style={styles.backButtonText}>Back to Chat</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={{ gap: 8 }}
|
||||
>
|
||||
{/* Orchestrator pill */}
|
||||
<Pressable
|
||||
onPress={onSelectOrchestrator}
|
||||
style={({ pressed }) => [
|
||||
styles.processItem,
|
||||
viewMode === 'orchestrator' ? styles.processItemActive : styles.processItemInactive,
|
||||
pressed && { opacity: 0.7 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.agentIcon} />
|
||||
<Text style={[
|
||||
styles.processText,
|
||||
viewMode === 'orchestrator' && styles.processTextActive,
|
||||
]}>Orchestrator</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Agent pills */}
|
||||
{agents.map((agent) => {
|
||||
const modeName = getModeName(agent.currentModeId, agent.availableModes);
|
||||
const isActive = activeProcessType === 'agent' && activeProcessId === agent.id;
|
||||
const isActive = viewMode === 'agent' && activeAgentId === agent.id;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -184,7 +191,10 @@ export function ActiveProcesses({
|
||||
>
|
||||
<View style={styles.agentIcon} />
|
||||
|
||||
<Text style={styles.processText}>{agent.id.substring(0, 8)}</Text>
|
||||
<Text style={[
|
||||
styles.processText,
|
||||
isActive && styles.processTextActive,
|
||||
]}>{agent.id.substring(0, 8)}</Text>
|
||||
|
||||
<View style={[styles.statusDot, { backgroundColor: getAgentStatusColor(agent.status) }]} />
|
||||
|
||||
@@ -194,26 +204,6 @@ export function ActiveProcesses({
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
|
||||
{commands.map((command) => {
|
||||
const statusColor = command.isDead
|
||||
? command.exitCode === 0
|
||||
? '#22c55e'
|
||||
: '#ef4444'
|
||||
: '#3b82f6';
|
||||
|
||||
return (
|
||||
<View key={`command-${command.id}`} style={[styles.processItem, styles.processItemInactive]}>
|
||||
<View style={styles.commandIcon} />
|
||||
|
||||
<Text style={styles.processText} numberOfLines={1}>
|
||||
{command.name || command.currentCommand.substring(0, 20)}
|
||||
</Text>
|
||||
|
||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -267,7 +267,7 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
thoughtLabel: {
|
||||
color: theme.colors.palette.purple[400],
|
||||
color: theme.colors.palette.purple[500],
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
marginBottom: theme.spacing[1],
|
||||
@@ -319,7 +319,7 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
statusText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
expandIcon: {
|
||||
color: theme.colors.mutedForeground,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { View, Text, ScrollView, Pressable } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { StyleSheet } from 'react-native-unistyles';
|
||||
import type { AgentStatus } from '@server/server/acp/types';
|
||||
import type { SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import { AgentActivityItem } from './agent-activity';
|
||||
import { parseSessionUpdate, groupActivities, type AgentActivity } from '@/types/agent-activity';
|
||||
import { AssistantMessage } from './message';
|
||||
import type { StreamItem } from '@/types/stream';
|
||||
|
||||
export interface AgentStreamViewProps {
|
||||
agentId: string;
|
||||
@@ -15,13 +14,7 @@ export interface AgentStreamViewProps {
|
||||
createdAt: Date;
|
||||
type: 'claude';
|
||||
};
|
||||
updates: Array<{
|
||||
timestamp: Date;
|
||||
notification: SessionNotification;
|
||||
}>;
|
||||
onBack: () => void;
|
||||
onKillAgent: (agentId: string) => void;
|
||||
onCancelAgent: (agentId: string) => void;
|
||||
streamItems: StreamItem[];
|
||||
}
|
||||
|
||||
function getStatusColor(status: AgentStatus): string {
|
||||
@@ -60,6 +53,11 @@ function getStatusIcon(status: AgentStatus): string {
|
||||
}
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
// Handle invalid dates gracefully
|
||||
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
||||
return '--:--:--';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
@@ -68,124 +66,138 @@ function formatTimestamp(date: Date): string {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function UserMessageCard({ text, timestamp }: { text: string; timestamp: Date }) {
|
||||
return (
|
||||
<View style={stylesheet.userMessageCard}>
|
||||
<Text style={stylesheet.timestamp}>
|
||||
{formatTimestamp(timestamp)}
|
||||
</Text>
|
||||
<Text style={stylesheet.userMessageLabel}>You</Text>
|
||||
<Text style={stylesheet.userMessageText}>{text}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ThoughtCard({ text, timestamp }: { text: string; timestamp: Date }) {
|
||||
return (
|
||||
<View style={stylesheet.thoughtCard}>
|
||||
<Text style={stylesheet.timestamp}>
|
||||
{formatTimestamp(timestamp)}
|
||||
</Text>
|
||||
<Text style={stylesheet.thoughtLabel}>Thinking</Text>
|
||||
<Text style={stylesheet.thoughtText}>{text}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallCard({
|
||||
title,
|
||||
status,
|
||||
toolKind,
|
||||
timestamp
|
||||
}: {
|
||||
title: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
toolKind?: string;
|
||||
timestamp: Date;
|
||||
}) {
|
||||
return (
|
||||
<View style={stylesheet.toolCallCard}>
|
||||
<Text style={stylesheet.timestamp}>
|
||||
{formatTimestamp(timestamp)}
|
||||
</Text>
|
||||
<View style={stylesheet.toolCallHeader}>
|
||||
<Text style={stylesheet.toolCallLabel}>{toolKind || 'Tool'}</Text>
|
||||
<Text style={[stylesheet.toolCallStatus, { color: getStatusColor(status as any) }]}>
|
||||
{status}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={stylesheet.toolCallTitle}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentStreamView({
|
||||
agentId,
|
||||
agent,
|
||||
updates,
|
||||
onBack,
|
||||
onKillAgent,
|
||||
onCancelAgent,
|
||||
streamItems,
|
||||
}: AgentStreamViewProps) {
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// Parse and group activities
|
||||
const groupedActivities = useMemo(() => {
|
||||
// Parse all notifications into typed activities
|
||||
const activities: AgentActivity[] = updates
|
||||
.map((update) => {
|
||||
const parsedUpdate = parseSessionUpdate(update.notification);
|
||||
if (!parsedUpdate) return null;
|
||||
|
||||
return {
|
||||
timestamp: update.timestamp,
|
||||
update: parsedUpdate,
|
||||
};
|
||||
})
|
||||
.filter((activity): activity is AgentActivity => activity !== null);
|
||||
|
||||
// Group consecutive text chunks and merge tool calls
|
||||
return groupActivities(activities);
|
||||
}, [updates]);
|
||||
|
||||
// Auto-scroll to bottom when new updates arrive
|
||||
// Auto-scroll to bottom when new items arrive
|
||||
useEffect(() => {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
}, [groupedActivities]);
|
||||
|
||||
const canCancel = agent.status === 'processing';
|
||||
const canKill = agent.status !== 'killed' && agent.status !== 'completed';
|
||||
}, [streamItems]);
|
||||
|
||||
return (
|
||||
<View style={stylesheet.container}>
|
||||
{/* Header */}
|
||||
<View style={[stylesheet.header, { paddingTop: insets.top + 16 }]}>
|
||||
{/* Back button */}
|
||||
<Pressable
|
||||
onPress={onBack}
|
||||
style={stylesheet.backButton}
|
||||
>
|
||||
<Text style={stylesheet.backButtonText}>
|
||||
← Back to Chat
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Agent info */}
|
||||
<View style={stylesheet.agentInfoRow}>
|
||||
<View style={stylesheet.agentIdRow}>
|
||||
<Text style={stylesheet.agentLabel}>Agent:</Text>
|
||||
<Text style={stylesheet.agentId}>
|
||||
{agentId.substring(0, 8)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
stylesheet.statusBadge,
|
||||
{ backgroundColor: getStatusColor(agent.status) }
|
||||
]}
|
||||
>
|
||||
<Text style={stylesheet.statusIcon}>{getStatusIcon(agent.status)}</Text>
|
||||
<Text style={stylesheet.statusText}>{agent.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={stylesheet.createdText}>
|
||||
Created: {formatTimestamp(agent.createdAt)}
|
||||
</Text>
|
||||
|
||||
{/* Control buttons */}
|
||||
{(canCancel || canKill) && (
|
||||
<View style={stylesheet.controlButtons}>
|
||||
{canCancel && (
|
||||
<Pressable
|
||||
onPress={() => onCancelAgent(agentId)}
|
||||
style={stylesheet.cancelButton}
|
||||
>
|
||||
<Text style={stylesheet.controlButtonText}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
{canKill && (
|
||||
<Pressable
|
||||
onPress={() => onKillAgent(agentId)}
|
||||
style={stylesheet.killButton}
|
||||
>
|
||||
<Text style={stylesheet.controlButtonText}>
|
||||
Kill
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Updates list */}
|
||||
{/* Content list */}
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={stylesheet.scrollView}
|
||||
contentContainerStyle={{ paddingTop: 16, paddingBottom: Math.max(insets.bottom, 32) }}
|
||||
>
|
||||
{groupedActivities.length === 0 ? (
|
||||
{streamItems.length === 0 ? (
|
||||
<View style={stylesheet.emptyState}>
|
||||
<Text style={stylesheet.emptyStateText}>
|
||||
No updates yet.{'\n'}Waiting for agent activity...
|
||||
Start chatting with this agent...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
groupedActivities.map((item, idx) => (
|
||||
<AgentActivityItem key={idx} item={item} />
|
||||
))
|
||||
streamItems.map((item) => {
|
||||
switch (item.kind) {
|
||||
case 'user_message':
|
||||
return (
|
||||
<UserMessageCard
|
||||
key={item.id}
|
||||
text={item.text}
|
||||
timestamp={item.timestamp}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'assistant_message':
|
||||
return (
|
||||
<AssistantMessage
|
||||
key={item.id}
|
||||
message={item.text}
|
||||
timestamp={item.timestamp.getTime()}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'thought':
|
||||
return (
|
||||
<ThoughtCard
|
||||
key={item.id}
|
||||
text={item.text}
|
||||
timestamp={item.timestamp}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'tool_call':
|
||||
return (
|
||||
<ToolCallCard
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
status={item.status}
|
||||
toolKind={item.toolKind}
|
||||
timestamp={item.timestamp}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'plan':
|
||||
// TODO: Render plan component
|
||||
return null;
|
||||
|
||||
case 'activity_log':
|
||||
case 'artifact':
|
||||
// These are orchestrator-only, skip for now
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
@@ -197,6 +209,78 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
userMessageCard: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
padding: theme.spacing[3],
|
||||
marginBottom: theme.spacing[2],
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: theme.colors.primary,
|
||||
},
|
||||
userMessageLabel: {
|
||||
color: theme.colors.primary,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
userMessageText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
timestamp: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
thoughtCard: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
padding: theme.spacing[3],
|
||||
marginBottom: theme.spacing[2],
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: theme.colors.palette.purple[500],
|
||||
},
|
||||
thoughtLabel: {
|
||||
color: theme.colors.palette.purple[500],
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
thoughtText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
lineHeight: 20,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
toolCallCard: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
padding: theme.spacing[3],
|
||||
marginBottom: theme.spacing[2],
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: theme.colors.palette.blue[500],
|
||||
},
|
||||
toolCallHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
toolCallLabel: {
|
||||
color: theme.colors.palette.blue[500],
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
toolCallStatus: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
toolCallTitle: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { X } from "lucide-react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { theme as defaultTheme } from "@/styles/theme";
|
||||
|
||||
interface CreateAgentModalProps {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
onCreateAgent: (workingDir: string, mode: string) => void;
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
{
|
||||
value: "plan",
|
||||
label: "Plan",
|
||||
description: "Plan and design before implementing",
|
||||
},
|
||||
{
|
||||
value: "bypassPermissions",
|
||||
label: "Bypass Permissions",
|
||||
description: "Skip permission prompts for faster execution",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function CreateAgentModal({
|
||||
isVisible,
|
||||
onClose,
|
||||
onCreateAgent,
|
||||
}: CreateAgentModalProps) {
|
||||
const [workingDir, setWorkingDir] = useState("");
|
||||
const [selectedMode, setSelectedMode] = useState("plan");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
function handleCreate() {
|
||||
if (!workingDir.trim()) {
|
||||
setErrorMessage("Working directory is required");
|
||||
return;
|
||||
}
|
||||
|
||||
onCreateAgent(workingDir.trim(), selectedMode);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setWorkingDir("");
|
||||
setSelectedMode("plan");
|
||||
setErrorMessage("");
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.modalOverlay}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<Pressable style={styles.modalBackdrop} onPress={handleClose} />
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalInner}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Create New Agent</Text>
|
||||
<Pressable onPress={handleClose}>
|
||||
<X size={24} color="white" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
>
|
||||
{/* Working Directory Input */}
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.label}>Working Directory</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="/path/to/project"
|
||||
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
||||
value={workingDir}
|
||||
onChangeText={(text) => {
|
||||
setWorkingDir(text);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
{errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
) : (
|
||||
<Text style={styles.helperText}>
|
||||
Absolute path to the project directory
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Mode Selector */}
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.label}>Mode</Text>
|
||||
<View style={styles.modeContainer}>
|
||||
{MODES.map((mode) => (
|
||||
<Pressable
|
||||
key={mode.value}
|
||||
onPress={() => setSelectedMode(mode.value)}
|
||||
style={[
|
||||
styles.modeOption,
|
||||
selectedMode === mode.value &&
|
||||
styles.modeOptionSelected,
|
||||
]}
|
||||
>
|
||||
<View style={styles.modeOptionContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.radioOuter,
|
||||
selectedMode === mode.value
|
||||
? styles.radioOuterSelected
|
||||
: styles.radioOuterUnselected,
|
||||
]}
|
||||
>
|
||||
{selectedMode === mode.value && (
|
||||
<View style={styles.radioInner} />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.modeTextContainer}>
|
||||
<Text style={styles.modeLabel}>{mode.label}</Text>
|
||||
<Text style={styles.modeDescription}>
|
||||
{mode.description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable
|
||||
style={[styles.createButton, !workingDir.trim() && styles.createButtonDisabled]}
|
||||
onPress={handleCreate}
|
||||
disabled={!workingDir.trim()}
|
||||
>
|
||||
<Text style={styles.createButtonText}>Create Agent</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable style={styles.cancelButton} onPress={handleClose}>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
},
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderTopLeftRadius: theme.spacing[6],
|
||||
borderTopRightRadius: theme.spacing[6],
|
||||
height: "75%",
|
||||
},
|
||||
modalInner: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing[6],
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerTitle: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: theme.spacing[6],
|
||||
paddingBottom: theme.spacing[4] * 5,
|
||||
},
|
||||
formSection: {
|
||||
marginBottom: theme.spacing[6],
|
||||
},
|
||||
label: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
input: {
|
||||
backgroundColor: theme.colors.background,
|
||||
color: theme.colors.foreground,
|
||||
padding: theme.spacing[4],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
helperText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
marginTop: theme.spacing[2],
|
||||
},
|
||||
errorText: {
|
||||
color: theme.colors.destructive,
|
||||
fontSize: theme.fontSize.sm,
|
||||
marginTop: theme.spacing[2],
|
||||
},
|
||||
modeContainer: {
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
modeOption: {
|
||||
backgroundColor: theme.colors.background,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
padding: theme.spacing[4],
|
||||
},
|
||||
modeOptionSelected: {
|
||||
borderColor: theme.colors.palette.blue[500],
|
||||
backgroundColor: theme.colors.muted,
|
||||
},
|
||||
modeOptionContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
radioOuter: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: theme.borderWidth[2],
|
||||
marginRight: theme.spacing[3],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
radioOuterSelected: {
|
||||
borderColor: theme.colors.palette.blue[500],
|
||||
},
|
||||
radioOuterUnselected: {
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
radioInner: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.palette.blue[500],
|
||||
},
|
||||
modeTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
modeLabel: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
modeDescription: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
buttonContainer: {
|
||||
gap: theme.spacing[3],
|
||||
marginTop: theme.spacing[4],
|
||||
},
|
||||
createButton: {
|
||||
backgroundColor: theme.colors.palette.blue[500],
|
||||
paddingVertical: theme.spacing[4],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
alignItems: "center",
|
||||
},
|
||||
createButtonDisabled: {
|
||||
backgroundColor: theme.colors.palette.blue[900],
|
||||
opacity: theme.opacity[50],
|
||||
},
|
||||
createButtonText: {
|
||||
color: theme.colors.palette.white,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: "transparent",
|
||||
paddingVertical: theme.spacing[4],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
alignItems: "center",
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user