``` 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> ```
211 lines
5.7 KiB
TypeScript
211 lines
5.7 KiB
TypeScript
import { View, Text, Pressable, ScrollView } from 'react-native';
|
|
import { StyleSheet } from 'react-native-unistyles';
|
|
import type { AgentStatus } from '@server/server/acp/types';
|
|
|
|
export interface ActiveProcessesProps {
|
|
agents: Array<{
|
|
id: string;
|
|
status: AgentStatus;
|
|
type: 'claude';
|
|
currentModeId?: string;
|
|
availableModes?: Array<{ id: string; name: string; description?: string | null }>;
|
|
}>;
|
|
commands: Array<{
|
|
id: string;
|
|
name: string;
|
|
workingDirectory: string;
|
|
currentCommand: string;
|
|
isDead: boolean;
|
|
exitCode: number | null;
|
|
}>;
|
|
viewMode: 'orchestrator' | 'agent';
|
|
activeAgentId: string | null;
|
|
onSelectAgent: (id: string) => void;
|
|
onSelectOrchestrator: () => void;
|
|
}
|
|
|
|
function getAgentStatusColor(status: AgentStatus): string {
|
|
switch (status) {
|
|
case 'initializing':
|
|
return '#f59e0b'; // orange
|
|
case 'ready':
|
|
return '#3b82f6'; // blue
|
|
case 'processing':
|
|
return '#fbbf24'; // yellow
|
|
case 'completed':
|
|
return '#22c55e'; // green
|
|
case 'failed':
|
|
return '#ef4444'; // red
|
|
case 'killed':
|
|
return '#9ca3af'; // gray
|
|
default:
|
|
return '#9ca3af';
|
|
}
|
|
}
|
|
|
|
function getModeName(modeId?: string, availableModes?: Array<{ id: string; name: string }>): string {
|
|
if (!modeId) return 'unknown';
|
|
const mode = availableModes?.find((m) => m.id === modeId);
|
|
return mode?.name || modeId;
|
|
}
|
|
|
|
function getModeColor(modeId?: string): string {
|
|
if (!modeId) return '#9ca3af'; // gray
|
|
|
|
// Color based on common mode types
|
|
if (modeId.includes('ask')) return '#f59e0b'; // orange - asks permission
|
|
if (modeId.includes('code')) return '#22c55e'; // green - writes code
|
|
if (modeId.includes('architect') || modeId.includes('plan')) return '#3b82f6'; // blue - plans
|
|
|
|
return '#9ca3af'; // gray - unknown
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
backgroundColor: theme.colors.card,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
header: {
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
backButton: {
|
|
backgroundColor: theme.colors.muted,
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[2],
|
|
borderRadius: theme.borderRadius.lg,
|
|
},
|
|
backButtonActive: {
|
|
backgroundColor: theme.colors.palette.zinc[700],
|
|
},
|
|
backButtonText: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: '600',
|
|
textAlign: 'center',
|
|
},
|
|
scrollView: {
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
},
|
|
processItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[3],
|
|
paddingVertical: theme.spacing[2],
|
|
borderRadius: theme.borderRadius.lg,
|
|
},
|
|
processItemActive: {
|
|
backgroundColor: theme.colors.primary,
|
|
},
|
|
processItemInactive: {
|
|
backgroundColor: theme.colors.muted,
|
|
},
|
|
agentIcon: {
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.palette.blue[500],
|
|
},
|
|
commandIcon: {
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: theme.borderRadius.sm,
|
|
backgroundColor: theme.colors.palette.purple[500],
|
|
},
|
|
processText: {
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.xs,
|
|
fontWeight: '500',
|
|
},
|
|
processTextActive: {
|
|
color: theme.colors.primaryForeground,
|
|
},
|
|
statusDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: theme.borderRadius.full,
|
|
},
|
|
modeIndicator: {
|
|
width: 6,
|
|
height: 6,
|
|
borderRadius: theme.borderRadius.full,
|
|
opacity: 0.3,
|
|
},
|
|
}));
|
|
|
|
export function ActiveProcesses({
|
|
agents,
|
|
commands,
|
|
viewMode,
|
|
activeAgentId,
|
|
onSelectAgent,
|
|
onSelectOrchestrator,
|
|
}: ActiveProcessesProps) {
|
|
// Only show if there's at least one agent
|
|
if (agents.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<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 isActive = viewMode === 'agent' && activeAgentId === agent.id;
|
|
|
|
return (
|
|
<Pressable
|
|
key={`agent-${agent.id}`}
|
|
onPress={() => onSelectAgent(agent.id)}
|
|
style={({ pressed }) => [
|
|
styles.processItem,
|
|
isActive ? styles.processItemActive : styles.processItemInactive,
|
|
pressed && { opacity: 0.7 },
|
|
]}
|
|
>
|
|
<View style={styles.agentIcon} />
|
|
|
|
<Text style={[
|
|
styles.processText,
|
|
isActive && styles.processTextActive,
|
|
]}>{agent.id.substring(0, 8)}</Text>
|
|
|
|
<View style={[styles.statusDot, { backgroundColor: getAgentStatusColor(agent.status) }]} />
|
|
|
|
{agent.currentModeId && (
|
|
<View style={[styles.modeIndicator, { backgroundColor: getModeColor(agent.currentModeId) }]} />
|
|
)}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|