diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index c5dc68638..cc09fa8d7 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -14,7 +14,7 @@ import { useSession } from "@/contexts/session-context"; export default function HomeScreen() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); - const { agents, createAgent } = useSession(); + const { agents } = useSession(); const [showCreateModal, setShowCreateModal] = useState(false); // Keyboard animation @@ -34,11 +34,6 @@ export default function HomeScreen() { setShowCreateModal(true); } - function handleCreateAgentConfirm(workingDir: string, mode: string) { - createAgent({ cwd: workingDir, autoStart: true }); - setShowCreateModal(false); - } - return ( {/* Header */} @@ -60,7 +55,6 @@ export default function HomeScreen() { setShowCreateModal(false)} - onCreateAgent={handleCreateAgentConfirm} /> ); diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index 083990d2c..d6e7e16f8 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect, useMemo, useCallback } from "react"; -import { View, Text, Pressable, ScrollView } from "react-native"; +import { View, Text, Pressable, ScrollView, ActivityIndicator } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import Animated, { useAnimatedStyle } from "react-native-reanimated"; @@ -18,11 +18,12 @@ import type { import { StyleSheet } from "react-native-unistyles"; import { theme as defaultTheme } from "@/styles/theme"; import { useRecentPaths } from "@/hooks/use-recent-paths"; +import { useSession } from "@/contexts/session-context"; +import { useRouter } from "expo-router"; interface CreateAgentModalProps { isVisible: boolean; onClose: () => void; - onCreateAgent: (workingDir: string, mode: string) => void; } const MODES = [ @@ -41,16 +42,19 @@ const MODES = [ export function CreateAgentModal({ isVisible, onClose, - onCreateAgent, }: CreateAgentModalProps) { const bottomSheetRef = useRef(null); const insets = useSafeAreaInsets(); const { recentPaths, addRecentPath } = useRecentPaths(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); + const { ws, createAgent } = useSession(); + const router = useRouter(); const [workingDir, setWorkingDir] = useState(""); const [selectedMode, setSelectedMode] = useState("plan"); const [errorMessage, setErrorMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [pendingRequestId, setPendingRequestId] = useState(null); const snapPoints = useMemo(() => ["90%"], []); @@ -86,17 +90,24 @@ export function CreateAgentModal({ - Create Agent + {isLoading ? ( + + + Creating... + + ) : ( + Create Agent + )} ), - [insets.bottom, workingDir, animatedFooterStyle] + [insets.bottom, workingDir, animatedFooterStyle, isLoading] ); useEffect(() => { @@ -105,12 +116,42 @@ export function CreateAgentModal({ } }, [isVisible]); + // Listen for agent_created events + useEffect(() => { + if (!pendingRequestId) return; + + const unsubscribe = ws.on("agent_created", (message) => { + if (message.type !== "agent_created") return; + + const { agentId, requestId } = message.payload; + + // Check if this is the response to our request + if (requestId === pendingRequestId) { + console.log("[CreateAgentModal] Agent created:", agentId); + setIsLoading(false); + setPendingRequestId(null); + handleClose(); + + // Navigate to the agent page + router.push(`/agent/${agentId}`); + } + }); + + return () => { + unsubscribe(); + }; + }, [pendingRequestId, ws, router]); + async function handleCreate() { if (!workingDir.trim()) { setErrorMessage("Working directory is required"); return; } + if (isLoading) { + return; + } + const path = workingDir.trim(); // Save to recent paths @@ -121,8 +162,26 @@ export function CreateAgentModal({ // Continue anyway - don't block agent creation } - onCreateAgent(path, selectedMode); - handleClose(); + // Generate request ID + const requestId = crypto.randomUUID(); + + setIsLoading(true); + setPendingRequestId(requestId); + setErrorMessage(""); + + // Create the agent + try { + createAgent({ + cwd: path, + initialMode: selectedMode, + requestId, + }); + } catch (error) { + console.error("[CreateAgentModal] Failed to create agent:", error); + setErrorMessage("Failed to create agent. Please try again."); + setIsLoading(false); + setPendingRequestId(null); + } } function handleClose() { @@ -130,9 +189,12 @@ export function CreateAgentModal({ } function handleDismiss() { + // Reset all state setWorkingDir(""); setSelectedMode("plan"); setErrorMessage(""); + setIsLoading(false); + setPendingRequestId(null); onClose(); } @@ -168,7 +230,7 @@ export function CreateAgentModal({ Working Directory {errorMessage && ( {errorMessage} @@ -214,9 +277,11 @@ export function CreateAgentModal({ setSelectedMode(mode.value)} + disabled={isLoading} style={[ styles.modeOption, selectedMode === mode.value && styles.modeOptionSelected, + isLoading && styles.modeOptionDisabled, ]} > @@ -296,6 +361,9 @@ const styles = StyleSheet.create((theme) => ({ borderColor: theme.colors.border, fontSize: theme.fontSize.base, }, + inputDisabled: { + opacity: theme.opacity[50], + }, helperText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, @@ -320,6 +388,9 @@ const styles = StyleSheet.create((theme) => ({ borderColor: theme.colors.palette.blue[500], backgroundColor: theme.colors.muted, }, + modeOptionDisabled: { + opacity: theme.opacity[50], + }, modeOptionContent: { flexDirection: "row", alignItems: "center", @@ -381,6 +452,11 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, + loadingContainer: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, recentPathsContainer: { flexDirection: "row", gap: theme.spacing[2], @@ -398,6 +474,6 @@ const styles = StyleSheet.create((theme) => ({ recentPathChipText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.medium, + fontWeight: theme.fontWeight.semibold, }, })); diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 99b6cbde8..33853dcca 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -120,7 +120,7 @@ interface SessionContextValue { // Helpers sendAgentMessage: (agentId: string, message: string) => void; - createAgent: (options: { cwd: string; autoStart?: boolean }) => void; + createAgent: (options: { cwd: string; initialMode?: string; requestId?: string }) => void; setAgentMode: (agentId: string, modeId: string) => void; respondToPermission: (requestId: string, agentId: string, sessionId: string, selectedOptionIds: string[]) => void; } @@ -259,14 +259,61 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { })); }); + // Audio output + const unsubAudioOutput = ws.on("audio_output", (message) => { + if (message.type !== "audio_output") return; + const { audio, format, id } = message.payload; + + console.log("[Session] Received audio output:", id, "format:", format); + + // Create Blob-like object for React Native + // React Native doesn't support creating Blobs from binary data + const audioBlob = { + type: format, + size: audio.length, + arrayBuffer: async () => { + // Convert base64 to ArrayBuffer + const binaryString = atob(audio); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + }, + } as Blob; + + // Play audio + setIsPlayingAudio(true); + audioPlayer.play(audioBlob) + .then(() => { + console.log("[Session] Audio playback complete:", id); + setIsPlayingAudio(false); + + // Send confirmation to server + const msg: WSInboundMessage = { + type: "session", + message: { + type: "audio_played", + id, + }, + }; + ws.send(msg); + }) + .catch((error) => { + console.error("[Session] Audio playback failed:", error); + setIsPlayingAudio(false); + }); + }); + return () => { unsubSessionState(); unsubAgentCreated(); unsubAgentStatus(); unsubAgentUpdate(); unsubPermissionRequest(); + unsubAudioOutput(); }; - }, [ws]); + }, [ws, audioPlayer, setIsPlayingAudio]); const sendAgentMessage = useCallback((agentId: string, message: string) => { // Generate unique message ID for deduplication @@ -310,7 +357,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { ws.send(msg); }, [ws]); - const createAgent = useCallback((options: { cwd: string; autoStart?: boolean }) => { + const createAgent = useCallback((options: { cwd: string; initialMode?: string; requestId?: string }) => { const msg: WSInboundMessage = { type: "session", message: { diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 06ba89fa5..426a2aaf4 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -63,6 +63,7 @@ export const CreateAgentRequestMessageSchema = z.object({ type: z.literal("create_agent_request"), cwd: z.string(), initialMode: z.string().optional(), + requestId: z.string().optional(), }); export const SetAgentModeMessageSchema = z.object({ @@ -188,6 +189,7 @@ export const AgentCreatedMessageSchema = z.object({ })).optional(), title: z.string().optional(), cwd: z.string(), + requestId: z.string().optional(), }), }); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 10a4f34c3..191c54348 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -466,7 +466,7 @@ export class Session { break; case "create_agent_request": - await this.handleCreateAgentRequest(msg.cwd, msg.initialMode); + await this.handleCreateAgentRequest(msg.cwd, msg.initialMode, msg.requestId); break; case "set_agent_mode": @@ -697,7 +697,8 @@ export class Session { */ private async handleCreateAgentRequest( cwd: string, - initialMode?: string + initialMode?: string, + requestId?: string ): Promise { console.log( `[Session ${this.clientId}] Creating agent in ${cwd} with mode ${ @@ -736,6 +737,7 @@ export class Session { availableModes: agentInfo?.availableModes ?? undefined, title: agentInfo?.title ?? undefined, cwd: agentInfo?.cwd || cwd, + requestId, }, }); console.log(