diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index 422b2ab98..588b3cf71 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -3,15 +3,14 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
-import { SessionProvider } from "@/contexts/session-context";
import { RealtimeProvider } from "@/contexts/realtime-context";
import { useAppSettings } from "@/hooks/use-settings";
import { View, ActivityIndicator, Text } from "react-native";
import { GlobalFooter } from "@/components/global-footer";
import { useUnistyles } from "react-native-unistyles";
import { FooterControlsProvider } from "@/contexts/footer-controls-context";
-import { DaemonRegistryProvider } from "@/contexts/daemon-registry-context";
-import { DaemonConnectionsProvider, useDaemonConnections } from "@/contexts/daemon-connections-context";
+import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
+import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
@@ -46,26 +45,19 @@ function AppContainer({ children }: { children: ReactNode }) {
}
function ProvidersWrapper({ children }: { children: ReactNode }) {
- const { settings, isLoading: settingsLoading } = useAppSettings();
- const { activeDaemon, isLoading: connectionsLoading } = useDaemonConnections();
+ const { isLoading: settingsLoading } = useAppSettings();
+ const { daemons, isLoading: registryLoading } = useDaemonRegistry();
+ const isLoading = settingsLoading || registryLoading;
- if (settingsLoading || connectionsLoading) {
+ if (isLoading) {
return ;
}
- if (!activeDaemon) {
+ if (daemons.length === 0) {
return ;
}
- return (
-
- {children}
-
- );
+ return {children};
}
function LoadingView() {
diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx
index d25aeca01..3d7ad105d 100644
--- a/packages/app/src/app/index.tsx
+++ b/packages/app/src/app/index.tsx
@@ -8,7 +8,6 @@ import { HomeHeader } from "@/components/headers/home-header";
import { EmptyState } from "@/components/empty-state";
import { AgentList } from "@/components/agent-list";
import { CreateAgentModal, ImportAgentModal } from "@/components/create-agent-modal";
-import { useSession } from "@/contexts/session-context";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus, getConnectionStatusTone, type ConnectionStatusTone } from "@/utils/daemons";
@@ -17,9 +16,8 @@ import { useLocalSearchParams } from "expo-router";
export default function HomeScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
- const { agents } = useSession();
const { connectionStates } = useDaemonConnections();
- const aggregatedAgents = useAggregatedAgents(agents);
+ const aggregatedAgents = useAggregatedAgents();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [createModalMounted, setCreateModalMounted] = useState(false);
diff --git a/packages/app/src/app/orchestrator.tsx b/packages/app/src/app/orchestrator.tsx
index bb54d8857..65cd3963b 100644
--- a/packages/app/src/app/orchestrator.tsx
+++ b/packages/app/src/app/orchestrator.tsx
@@ -1,21 +1,42 @@
import { View } from "react-native";
-import { useRef, useState } from "react";
+import { useMemo, useRef, useState } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header";
import { OrchestratorMessagesView } from "@/components/orchestrator-messages-view";
-import { useSession } from "@/contexts/session-context";
+import { useSessionDirectory } from "@/hooks/use-session-directory";
import type { ScrollView } from "react-native";
import type { Artifact } from "@/components/artifact-drawer";
+import type { MessageEntry } from "@/contexts/session-context";
export default function OrchestratorScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
- const { messages, currentAssistantMessage } = useSession();
+ const sessionDirectory = useSessionDirectory();
const scrollViewRef = useRef(null);
const [currentArtifact, setCurrentArtifact] = useState(null);
+ const aggregatedMessages = useMemo(() => {
+ const merged: MessageEntry[] = [];
+ sessionDirectory.forEach((session) => {
+ if (!session) {
+ return;
+ }
+ merged.push(...session.messages);
+ });
+ merged.sort((left, right) => left.timestamp - right.timestamp);
+ return merged;
+ }, [sessionDirectory]);
+
+ const streamingAssistantMessage = useMemo(() => {
+ for (const session of sessionDirectory.values()) {
+ if (session?.currentAssistantMessage) {
+ return session.currentAssistantMessage;
+ }
+ }
+ return "";
+ }, [sessionDirectory]);
// Keyboard animation
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
@@ -42,8 +63,8 @@ export default function OrchestratorScreen() {
diff --git a/packages/app/src/app/settings.tsx b/packages/app/src/app/settings.tsx
index 9cdca8a9b..fced3f170 100644
--- a/packages/app/src/app/settings.tsx
+++ b/packages/app/src/app/settings.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef, useCallback } from "react";
+import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import type { MutableRefObject } from "react";
import {
View,
@@ -59,6 +59,31 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.semibold,
marginBottom: theme.spacing[4],
},
+ hostSelectorRow: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: theme.spacing[2],
+ marginBottom: theme.spacing[3],
+ },
+ hostSelectorChip: {
+ paddingVertical: theme.spacing[1],
+ paddingHorizontal: theme.spacing[3],
+ borderRadius: theme.borderRadius.full,
+ backgroundColor: theme.colors.card,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.border,
+ },
+ hostSelectorChipSelected: {
+ backgroundColor: theme.colors.foreground,
+ borderColor: theme.colors.foreground,
+ },
+ hostSelectorChipText: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.xs,
+ },
+ hostSelectorChipTextSelected: {
+ color: theme.colors.background,
+ },
label: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.sm,
@@ -324,12 +349,19 @@ type DaemonTestState = {
export default function SettingsScreen() {
const { settings, isLoading: settingsLoading, updateSettings, resetSettings } = useAppSettings();
const { daemons, isLoading: daemonLoading, addDaemon, updateDaemon, removeDaemon } = useDaemonRegistry();
- const { activeDaemon, activeDaemonId, setActiveDaemonId, connectionStates, updateConnectionStatus } = useDaemonConnections();
- const activeDaemonSession = useSessionForServer(activeDaemonId ?? null);
- const activeDaemonConnection = activeDaemonId ? connectionStates.get(activeDaemonId) : null;
- const activeDaemonStatusLabel = activeDaemonConnection ? formatConnectionStatus(activeDaemonConnection.status) : null;
+ const { connectionStates, updateConnectionStatus } = useDaemonConnections();
+ const [selectedDaemonId, setSelectedDaemonId] = useState(null);
+ const selectedDaemon = useMemo(() => {
+ if (!selectedDaemonId) {
+ return null;
+ }
+ return daemons.find((daemon) => daemon.id === selectedDaemonId) ?? null;
+ }, [daemons, selectedDaemonId]);
+ const selectedDaemonSession = useSessionForServer(selectedDaemonId);
+ const selectedDaemonConnection = selectedDaemonId ? connectionStates.get(selectedDaemonId) : null;
+ const selectedDaemonStatusLabel = selectedDaemonConnection ? formatConnectionStatus(selectedDaemonConnection.status) : null;
- const [serverUrl, setServerUrl] = useState(activeDaemon?.wsUrl ?? "");
+ const [serverUrl, setServerUrl] = useState(selectedDaemon?.wsUrl ?? "");
const [useSpeaker, setUseSpeaker] = useState(settings.useSpeaker);
const [keepScreenOn, setKeepScreenOn] = useState(settings.keepScreenOn);
const [theme, setTheme] = useState<"dark" | "light" | "auto">(settings.theme);
@@ -346,18 +378,28 @@ export default function SettingsScreen() {
const [isSavingDaemon, setIsSavingDaemon] = useState(false);
const [daemonTestStates, setDaemonTestStates] = useState