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}
serverId={selectedServerId ?? ""}
onSubmitMessage={handleCreateFromInput}
isSubmitLoading={isLoading}
/>
</View>
</View>
@@ -46,6 +46,12 @@ type QueuedMessage = {
interface AgentInputAreaProps {
agentId: 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;
@@ -85,7 +91,12 @@ type DictationToastConfig = {
onDismiss?: () => void;
};
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
export function AgentInputArea({
agentId,
serverId,
onSubmitMessage,
isSubmitLoading = false,
}: AgentInputAreaProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
@@ -153,10 +164,21 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED;
const agentIdRef = useRef(agentId);
const sendAgentMessageRef = useRef(sendAgentMessage);
const onSubmitMessageRef = useRef(onSubmitMessage);
const agentStatusRef = useRef<string | undefined>(undefined);
const updateQueueRef = useRef<
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | 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(
(text: string) => {
if (!text) {
@@ -175,7 +197,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}
void (async () => {
try {
await sendAgentMessageRef.current?.(agentIdRef.current, text);
await submitMessage(text);
} catch (error) {
console.error("[AgentInput] Failed to send transcribed message:", error);
updateQueueRef.current?.((current) => [
@@ -188,7 +210,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}
})();
},
[]
[submitMessage]
);
const handleDictationError = useCallback((error: Error) => {
@@ -197,24 +219,24 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const canStartDictation = useCallback(() => {
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = !isRealtimeMode && socketConnected;
const allowed = !isRealtimeMode && socketConnected && Boolean(agent);
console.log("[AgentInput] canStartDictation", {
allowed,
isRealtimeMode,
wsConnected: socketConnected,
});
return allowed;
}, [isRealtimeMode, wsOrInert]);
}, [agent, isRealtimeMode, wsOrInert]);
const canConfirmDictation = useCallback(() => {
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = socketConnected;
const allowed = socketConnected && Boolean(agent);
console.log("[AgentInput] canConfirmDictation", {
allowed,
wsConnected: socketConnected,
});
return allowed;
}, [wsOrInert]);
}, [agent, wsOrInert]);
const {
isRecording: isDictating,
@@ -334,9 +356,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
sendAgentMessageRef.current = sendAgentMessage;
}, [sendAgentMessage]);
useEffect(() => {
onSubmitMessageRef.current = onSubmitMessage;
}, [onSubmitMessage]);
async function handleSendMessage() {
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 imageAttachments = selectedImages.length > 0 ? selectedImages : undefined;
@@ -347,7 +374,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
setIsProcessing(true);
try {
await sendAgentMessage(agentIdRef.current, message, imageAttachments);
await submitMessage(message, imageAttachments);
} catch (error) {
console.error("[AgentInput] Failed to send message:", error);
} finally {
@@ -834,13 +861,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
async function handleSendQueuedNow(id: string) {
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));
// Cancels current agent run before sending queued prompt
handleCancelAgent();
await sendAgentMessage(agentIdRef.current, item.text, item.images);
await submitMessage(item.text, item.images);
}
const realtimeButton = (
@@ -1019,13 +1047,17 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
) : shouldShowSendButton ? (
<Pressable
onPress={handleSendMessage}
disabled={!wsOrInert.isConnected || isProcessing}
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
style={[
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>
) : shouldShowVoiceControls ? (
<>