diff --git a/packages/app/src/app/agent/[...route].tsx b/packages/app/src/app/agent/[...route].tsx deleted file mode 100644 index eccbac009..000000000 --- a/packages/app/src/app/agent/[...route].tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useLocalSearchParams } from "expo-router"; -import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen"; -import { LegacyAgentIdScreen } from "@/screens/agent/legacy-agent-id-screen"; - -type AgentRouteParams = { - route?: string[] | string; -}; - -function normalizeSegments(raw: AgentRouteParams["route"]): string[] { - if (!raw) return []; - if (Array.isArray(raw)) return raw.filter((s) => typeof s === "string" && s.length > 0); - if (typeof raw === "string") return raw.length > 0 ? [raw] : []; - return []; -} - -export default function AgentCatchAllRoute() { - const params = useLocalSearchParams(); - const segments = normalizeSegments(params.route); - - if (segments.length === 1) { - return ; - } - - if (segments.length >= 2) { - const [serverId, agentId] = segments; - return ; - } - - return ; -} - diff --git a/packages/app/src/app/agent/[[...route]].tsx b/packages/app/src/app/agent/[[...route]].tsx new file mode 100644 index 000000000..eff908df8 --- /dev/null +++ b/packages/app/src/app/agent/[[...route]].tsx @@ -0,0 +1,109 @@ +import { useEffect, useMemo, useState } from "react"; +import { View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; +import { StyleSheet } from "react-native-unistyles"; +import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen"; +import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen"; +import { LegacyAgentIdScreen } from "@/screens/agent/legacy-agent-id-screen"; +import { useSessionStore } from "@/stores/session-store"; + +type AgentRouteParams = { + route?: string[] | string; +}; + +function normalizeSegments(raw: AgentRouteParams["route"]): string[] { + if (!raw) return []; + if (Array.isArray(raw)) return raw.filter((s) => typeof s === "string" && s.length > 0); + if (typeof raw === "string") return raw.length > 0 ? [raw] : []; + return []; +} + +type RouteKind = "draft" | "legacy" | "ready"; + +export default function AgentRoute() { + const params = useLocalSearchParams(); + const segments = useMemo(() => normalizeSegments(params.route), [params.route]); + + const routeKind: RouteKind = + segments.length === 0 ? "draft" : segments.length === 1 ? "legacy" : "ready"; + const legacyAgentId = segments.length === 1 ? segments[0] : ""; + const serverId = segments.length >= 2 ? segments[0] : ""; + const agentId = segments.length >= 2 ? segments[1] : ""; + + const agent = useSessionStore((state) => + serverId && agentId ? state.sessions[serverId]?.agents?.get(agentId) : undefined + ); + const isInitializingFromMap = useSessionStore((state) => + serverId && agentId + ? state.sessions[serverId]?.initializingAgents?.get(agentId) ?? false + : false + ); + const isInitializing = agentId ? isInitializingFromMap !== false : false; + const isAgentReady = Boolean(agent && !isInitializing); + + const [createFlowActive, setCreateFlowActive] = useState(false); + const [shouldMountDraft, setShouldMountDraft] = useState(routeKind === "draft"); + + useEffect(() => { + if (routeKind === "draft") { + setShouldMountDraft(true); + } + }, [routeKind]); + + const ensureAgentIsInitialized = useSessionStore((state) => + serverId ? state.sessions[serverId]?.methods?.ensureAgentIsInitialized : undefined + ); + const isConnected = useSessionStore((state) => + serverId ? state.sessions[serverId]?.connection.isConnected ?? false : false + ); + + useEffect(() => { + if (routeKind !== "ready") return; + if (!agentId || !ensureAgentIsInitialized) return; + if (!isConnected) return; + + ensureAgentIsInitialized(agentId).catch((error) => { + console.warn("[AgentRoute] Agent initialization failed", { + agentId, + serverId, + error, + }); + }); + }, [agentId, ensureAgentIsInitialized, isConnected, routeKind, serverId]); + + useEffect(() => { + if (routeKind !== "ready") { + return; + } + if (createFlowActive && isAgentReady) { + setCreateFlowActive(false); + } + }, [createFlowActive, isAgentReady, routeKind]); + + if (routeKind === "legacy") { + return ; + } + + const shouldShowDraft = + routeKind === "draft" || (routeKind === "ready" && createFlowActive && !isAgentReady); + const shouldShowReady = routeKind === "ready" && (!createFlowActive || isAgentReady); + + return ( + + {shouldShowReady ? : null} + {shouldMountDraft && shouldShowDraft ? ( + + ) : null} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + backgroundColor: theme.colors.surface0, + }, +})); diff --git a/packages/app/src/app/agent/index.tsx b/packages/app/src/app/agent/index.tsx deleted file mode 100644 index 4a1a5c7d7..000000000 --- a/packages/app/src/app/agent/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen"; - -export default function AgentDraftRoute() { - return ; -} - diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index 664bd6809..ba3a61623 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -85,10 +85,8 @@ export function AgentList({ onAgentSelect?.(); navigate({ - pathname: "/agent/[...route]", - params: { - route: [serverId, agentId], - }, + pathname: "/agent/[[...route]]", + params: { route: [serverId, agentId] }, }); }, [isActionSheetVisible, pathname, onAgentSelect] diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 89d3a5dd2..87fc01d10 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -607,7 +607,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList", }); - router.replace("/agent"); + router.replace({ pathname: "/agent/[[...route]]" }); }, onError: (err) => { const message = err instanceof Error ? err.message : "Failed to archive worktree"; diff --git a/packages/app/src/components/home-footer.tsx b/packages/app/src/components/home-footer.tsx index 1b446a19e..d0863750c 100644 --- a/packages/app/src/components/home-footer.tsx +++ b/packages/app/src/components/home-footer.tsx @@ -142,7 +142,7 @@ export function HomeFooter() { { console.log("[HomeFooter] New Agent button pressed"); - router.push("/agent"); + router.push({ pathname: "/agent/[[...route]]" }); }} style={({ pressed }) => [ styles.footerButton, diff --git a/packages/app/src/components/sliding-sidebar.tsx b/packages/app/src/components/sliding-sidebar.tsx index e21175512..30c536b3b 100644 --- a/packages/app/src/components/sliding-sidebar.tsx +++ b/packages/app/src/components/sliding-sidebar.tsx @@ -82,7 +82,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) { }, [closeToAgent]); const handleCreateAgentClean = useCallback(() => { - router.push("/agent"); + router.push({ pathname: "/agent/[[...route]]" }); }, []); // Mobile: close sidebar and navigate diff --git a/packages/app/src/screens/agent/agent-ready-screen.tsx b/packages/app/src/screens/agent/agent-ready-screen.tsx index 33da601bf..d53e40411 100644 --- a/packages/app/src/screens/agent/agent-ready-screen.tsx +++ b/packages/app/src/screens/agent/agent-ready-screen.tsx @@ -118,7 +118,7 @@ export function AgentReadyScreen({ targetMs: 300, }); } - router.replace("/agent"); + router.replace({ pathname: "/agent/[[...route]]" }); }, [resolvedAgentId, resolvedServerId, router]); const focusServerId = resolvedServerId; diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index f16837ad0..ae42c406b 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -103,7 +103,15 @@ type DraftAgentParams = { workingDir?: string; }; -export function DraftAgentScreen() { +type DraftAgentScreenProps = { + isVisible?: boolean; + onCreateFlowActiveChange?: (active: boolean) => void; +}; + +export function DraftAgentScreen({ + isVisible = true, + onCreateFlowActiveChange, +}: DraftAgentScreenProps = {}) { const { theme } = useUnistyles(); const router = useRouter(); const insets = useSafeAreaInsets(); @@ -171,7 +179,7 @@ export function DraftAgentScreen() { } = useAgentFormState({ initialServerId: resolvedServerId ?? null, initialValues, - isVisible: true, + isVisible, isCreateFlow: true, }); const hostEntry = selectedServerId @@ -695,6 +703,7 @@ export function DraftAgentScreen() { } Keyboard.dismiss(); dispatch({ type: "SUBMIT", attempt }); + onCreateFlowActiveChange?.(true); try { const result = await createAgent({ @@ -707,7 +716,7 @@ export function DraftAgentScreen() { const agentId = (result as { id?: string })?.id; if (agentId && selectedServerId) { router.replace({ - pathname: "/agent/[...route]", + pathname: "/agent/[[...route]]", params: { route: [selectedServerId, agentId] }, }); return; @@ -717,10 +726,12 @@ export function DraftAgentScreen() { type: "CREATE_FAILED", message: "Failed to create agent", }); + onCreateFlowActiveChange?.(false); throw new Error("Failed to create agent"); } catch (error) { const message = error instanceof Error ? error.message : "Failed to create agent"; dispatch({ type: "CREATE_FAILED", message }); + onCreateFlowActiveChange?.(false); throw error; // Re-throw so AgentInputArea knows it failed } }, @@ -745,6 +756,7 @@ export function DraftAgentScreen() { workingDir, isAttachWorktree, isSubmitting, + onCreateFlowActiveChange, ] ); diff --git a/packages/app/src/screens/agent/legacy-agent-id-screen.tsx b/packages/app/src/screens/agent/legacy-agent-id-screen.tsx index 208ae10f8..4f294ee9d 100644 --- a/packages/app/src/screens/agent/legacy-agent-id-screen.tsx +++ b/packages/app/src/screens/agent/legacy-agent-id-screen.tsx @@ -51,19 +51,19 @@ export function LegacyAgentIdScreen({ agentId }: { agentId: string }) { } const match = matches[0]; router.replace({ - pathname: "/agent/[...route]", + pathname: "/agent/[[...route]]", params: { route: [match.serverId, match.agent.id] }, }); }, [isRedirecting, matches, router]); const handleGoDraft = useCallback(() => { - router.replace("/agent"); + router.replace({ pathname: "/agent/[[...route]]" }); }, [router]); const handleSelectMatch = useCallback( (match: AgentMatch) => { router.replace({ - pathname: "/agent/[...route]", + pathname: "/agent/[[...route]]", params: { route: [match.serverId, match.agent.id] }, }); },