Add visual loading indicator to new agent page

- Add isSubmitLoading prop to AgentInputArea component to allow parent
  to control loading state externally
- When isSubmitLoading is true:
  - Show ActivityIndicator instead of ArrowUp icon in send button
  - Disable send button with reduced opacity
- Pass isLoading state from new.tsx to AgentInputArea via isSubmitLoading prop

Files changed:
- packages/app/src/components/agent-input-area.tsx:53-54, 98, 1050-1060
- packages/app/src/app/agent/new.tsx:472

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 21:07:46 +07:00
co-authored by Claude Opus 4.5
parent 58769e7ece
commit bd7752b4f4
2 changed files with 47 additions and 14 deletions
+1
View File
@@ -469,6 +469,7 @@ export default function DraftAgentScreen() {
agentId={DRAFT_AGENT_ID} agentId={DRAFT_AGENT_ID}
serverId={selectedServerId ?? ""} serverId={selectedServerId ?? ""}
onSubmitMessage={handleCreateFromInput} onSubmitMessage={handleCreateFromInput}
isSubmitLoading={isLoading}
/> />
</View> </View>
</View> </View>
@@ -46,6 +46,12 @@ type QueuedMessage = {
interface AgentInputAreaProps { interface AgentInputAreaProps {
agentId: string; agentId: string;
serverId: string; serverId: string;
onSubmitMessage?: (payload: {
text: string;
images?: Array<{ uri: string; mimeType: string }>;
}) => Promise<void>;
/** Externally controlled loading state. When true, disables the submit button. */
isSubmitLoading?: boolean;
} }
const MIN_INPUT_HEIGHT = 50; const MIN_INPUT_HEIGHT = 50;
@@ -85,7 +91,12 @@ type DictationToastConfig = {
onDismiss?: () => void; onDismiss?: () => void;
}; };
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) { export function AgentInputArea({
agentId,
serverId,
onSubmitMessage,
isSubmitLoading = false,
}: AgentInputAreaProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
@@ -153,10 +164,21 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED; const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED;
const agentIdRef = useRef(agentId); const agentIdRef = useRef(agentId);
const sendAgentMessageRef = useRef(sendAgentMessage); const sendAgentMessageRef = useRef(sendAgentMessage);
const onSubmitMessageRef = useRef(onSubmitMessage);
const agentStatusRef = useRef<string | undefined>(undefined); const agentStatusRef = useRef<string | undefined>(undefined);
const updateQueueRef = useRef< const updateQueueRef = useRef<
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null ((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null
>(null); >(null);
const submitMessage = useCallback(
async (text: string, images?: Array<{ uri: string; mimeType: string }>) => {
if (onSubmitMessageRef.current) {
await onSubmitMessageRef.current({ text, images });
return;
}
await sendAgentMessageRef.current?.(agentIdRef.current, text, images);
},
[]
);
const handleDictationTranscript = useCallback( const handleDictationTranscript = useCallback(
(text: string) => { (text: string) => {
if (!text) { if (!text) {
@@ -175,7 +197,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
} }
void (async () => { void (async () => {
try { try {
await sendAgentMessageRef.current?.(agentIdRef.current, text); await submitMessage(text);
} catch (error) { } catch (error) {
console.error("[AgentInput] Failed to send transcribed message:", error); console.error("[AgentInput] Failed to send transcribed message:", error);
updateQueueRef.current?.((current) => [ updateQueueRef.current?.((current) => [
@@ -188,7 +210,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
} }
})(); })();
}, },
[] [submitMessage]
); );
const handleDictationError = useCallback((error: Error) => { const handleDictationError = useCallback((error: Error) => {
@@ -197,24 +219,24 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const canStartDictation = useCallback(() => { const canStartDictation = useCallback(() => {
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected; const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = !isRealtimeMode && socketConnected; const allowed = !isRealtimeMode && socketConnected && Boolean(agent);
console.log("[AgentInput] canStartDictation", { console.log("[AgentInput] canStartDictation", {
allowed, allowed,
isRealtimeMode, isRealtimeMode,
wsConnected: socketConnected, wsConnected: socketConnected,
}); });
return allowed; return allowed;
}, [isRealtimeMode, wsOrInert]); }, [agent, isRealtimeMode, wsOrInert]);
const canConfirmDictation = useCallback(() => { const canConfirmDictation = useCallback(() => {
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected; const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = socketConnected; const allowed = socketConnected && Boolean(agent);
console.log("[AgentInput] canConfirmDictation", { console.log("[AgentInput] canConfirmDictation", {
allowed, allowed,
wsConnected: socketConnected, wsConnected: socketConnected,
}); });
return allowed; return allowed;
}, [wsOrInert]); }, [agent, wsOrInert]);
const { const {
isRecording: isDictating, isRecording: isDictating,
@@ -334,9 +356,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
sendAgentMessageRef.current = sendAgentMessage; sendAgentMessageRef.current = sendAgentMessage;
}, [sendAgentMessage]); }, [sendAgentMessage]);
useEffect(() => {
onSubmitMessageRef.current = onSubmitMessage;
}, [onSubmitMessage]);
async function handleSendMessage() { async function handleSendMessage() {
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected; const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
if (!userInput.trim() || !socketConnected || !sendAgentMessage) return; if (!userInput.trim() || !socketConnected) return;
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
const message = userInput.trim(); const message = userInput.trim();
const imageAttachments = selectedImages.length > 0 ? selectedImages : undefined; const imageAttachments = selectedImages.length > 0 ? selectedImages : undefined;
@@ -347,7 +374,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
setIsProcessing(true); setIsProcessing(true);
try { try {
await sendAgentMessage(agentIdRef.current, message, imageAttachments); await submitMessage(message, imageAttachments);
} catch (error) { } catch (error) {
console.error("[AgentInput] Failed to send message:", error); console.error("[AgentInput] Failed to send message:", error);
} finally { } finally {
@@ -834,13 +861,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
async function handleSendQueuedNow(id: string) { async function handleSendQueuedNow(id: string) {
const item = queuedMessages.find((q) => q.id === id); const item = queuedMessages.find((q) => q.id === id);
if (!item || !wsOrInert.isConnected || !sendAgentMessage) return; if (!item || !wsOrInert.isConnected) return;
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
updateQueue((current) => current.filter((q) => q.id !== id)); updateQueue((current) => current.filter((q) => q.id !== id));
// Cancels current agent run before sending queued prompt // Cancels current agent run before sending queued prompt
handleCancelAgent(); handleCancelAgent();
await sendAgentMessage(agentIdRef.current, item.text, item.images); await submitMessage(item.text, item.images);
} }
const realtimeButton = ( const realtimeButton = (
@@ -1019,13 +1047,17 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
) : shouldShowSendButton ? ( ) : shouldShowSendButton ? (
<Pressable <Pressable
onPress={handleSendMessage} onPress={handleSendMessage}
disabled={!wsOrInert.isConnected || isProcessing} disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
style={[ style={[
styles.sendButton as any, styles.sendButton as any,
(!wsOrInert.isConnected || isProcessing ? styles.buttonDisabled : undefined) as any, (!wsOrInert.isConnected || isProcessing || isSubmitLoading ? styles.buttonDisabled : undefined) as any,
]} ]}
> >
<ArrowUp size={20} color="white" /> {isSubmitLoading ? (
<ActivityIndicator size="small" color="white" />
) : (
<ArrowUp size={20} color="white" />
)}
</Pressable> </Pressable>
) : shouldShowVoiceControls ? ( ) : shouldShowVoiceControls ? (
<> <>