diff --git a/frontend/mobile/src/components/chat/ChatInput.tsx b/frontend/mobile/src/components/chat/ChatInput.tsx index 85ecf6b4..5bee128a 100644 --- a/frontend/mobile/src/components/chat/ChatInput.tsx +++ b/frontend/mobile/src/components/chat/ChatInput.tsx @@ -291,6 +291,7 @@ export const ChatInput: React.FC = ({ editable={canWrite && !isSubmitting && !isTranscribing} onSubmitEditing={handleSubmit} blurOnSubmit={false} + autoCapitalize="none" /> )} diff --git a/frontend/mobile/src/components/ui/Header.tsx b/frontend/mobile/src/components/ui/Header.tsx index a451bd8f..94cd132e 100644 --- a/frontend/mobile/src/components/ui/Header.tsx +++ b/frontend/mobile/src/components/ui/Header.tsx @@ -62,7 +62,7 @@ const styles = StyleSheet.create({ borderBottomColor: theme.colors.borderDivider, }, headerSide: { - width: 32, // Standard width for side elements + minWidth: 32, // Minimum width for side elements justifyContent: 'center', alignItems: 'center', }, diff --git a/frontend/mobile/src/screens/dashboard/InstanceDetailScreen.tsx b/frontend/mobile/src/screens/dashboard/InstanceDetailScreen.tsx index fb62fe06..38159d7a 100644 --- a/frontend/mobile/src/screens/dashboard/InstanceDetailScreen.tsx +++ b/frontend/mobile/src/screens/dashboard/InstanceDetailScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { View, Text, @@ -16,7 +16,8 @@ import { Header } from '@/components/ui'; import { dashboardApi } from '@/services/api'; import { InstanceDetail, AgentStatus, Message, InstanceShare, InstanceAccessLevel } from '@/types'; import { formatAgentTypeName } from '@/utils/formatters'; -import { getStatusColor } from '@/utils/statusHelpers'; +import { withAlpha } from '@/lib/color'; +import { Pause, AlertTriangle, Power, CheckCircle, AlertCircle } from 'lucide-react-native'; import { ChatInterface } from '@/components/chat/ChatInterface'; import { useSSE } from '@/hooks/useSSE'; import { reportError, reportMessage } from '@/lib/sentry'; @@ -33,6 +34,7 @@ export const InstanceDetailScreen: React.FC = () => { const [instance, setInstance] = useState(null); const [sseEnabled, setSseEnabled] = useState(false); + const [now, setNow] = useState(() => Date.now()); const appStateRef = useRef(AppState.currentState); const sentryTags = { feature: 'mobile-instance-detail', instanceId }; @@ -111,6 +113,14 @@ export const InstanceDetailScreen: React.FC = () => { } }, [initialData]); + // Refresh the heartbeat comparison periodically so the status stays in sync + useEffect(() => { + const interval = setInterval(() => { + setNow(Date.now()); + }, 15000); + + return () => clearInterval(interval); + }, []); useEffect(() => { setShareModalVisible(false); setShares([]); @@ -327,6 +337,68 @@ export const InstanceDetailScreen: React.FC = () => { console.log('[InstanceDetailScreen] Query state - isLoading:', isLoading, 'instance:', !!instance, 'error:', !!error, 'sseEnabled:', sseEnabled); + const presence = useMemo(() => { + if (!instance) { + return { + label: 'Loading…', + dotColor: theme.colors.textMuted, + textColor: theme.colors.textMuted, + }; + } + + const ttlSeconds = 60; + const lastHeartbeat = instance.last_heartbeat_at ? new Date(instance.last_heartbeat_at).getTime() : null; + const secondsSince = lastHeartbeat ? Math.floor((now - lastHeartbeat) / 1000) : null; + const isOnline = secondsSince !== null && secondsSince <= ttlSeconds; + + if (!isOnline) { + return { + label: 'Offline', + dotColor: theme.colors.error, + textColor: theme.colors.error, + }; + } + + switch (instance.status) { + case AgentStatus.AWAITING_INPUT: + return { + label: 'Waiting', + dotColor: theme.colors.warning, + textColor: theme.colors.warning, + }; + case AgentStatus.PAUSED: + return { + label: 'Paused', + dotColor: theme.colors.info, + textColor: theme.colors.info, + }; + case AgentStatus.FAILED: + return { + label: 'Error', + dotColor: theme.colors.error, + textColor: theme.colors.error, + }; + case AgentStatus.KILLED: + return { + label: 'Stopped', + dotColor: theme.colors.error, + textColor: theme.colors.error, + }; + case AgentStatus.COMPLETED: + return { + label: 'Done', + dotColor: theme.colors.textMuted, + textColor: theme.colors.textMuted, + }; + default: + return { + label: 'Active', + dotColor: theme.colors.success, + textColor: theme.colors.success, + }; + } + }, [instance, now]); + if (isLoading) { console.log('[InstanceDetailScreen] Showing loading state'); return ( @@ -415,13 +487,19 @@ export const InstanceDetailScreen: React.FC = () => { {/* Header */} -
navigation.goBack()} - centerContent={ - - - {instance.name || formatAgentTypeName(instance.agent_type_name || 'Agent')} + rightContent={ + + {presence.label === 'Active' ? ( + + ) : ( + + )} + + {presence.label} + } rightContent={shareButton} @@ -462,21 +540,20 @@ const styles = StyleSheet.create({ safeArea: { flex: 1, }, - titleContainer: { + statusIndicator: { flexDirection: 'row', alignItems: 'center', + gap: 4, }, statusDot: { width: 8, height: 8, borderRadius: 4, - marginRight: theme.spacing.sm, }, - title: { - fontSize: theme.fontSize.lg, - fontFamily: theme.fontFamily.semibold, - fontWeight: theme.fontWeight.semibold as any, - color: theme.colors.white, + statusText: { + fontSize: theme.fontSize.xs, + fontFamily: theme.fontFamily.medium, + fontWeight: theme.fontWeight.medium as any, }, loadingContainer: { flex: 1,