Fix useDaemonSession to return combined state + APIs

Problem: After migrating to pure Zustand, useDaemonSession returned only
SessionState from the store, but components need both state AND imperative
APIs (like sendAgentMessage, createAgent, etc.).

Solution: Create DaemonSession type that combines:
- SessionState (from Zustand store)
- Imperative APIs (from SessionContext)
- Wrapped store actions (getDraftInput, setFocusedAgentId, etc.)

Changes:
- Added DaemonSession type to use-daemon-session.ts
- useDaemonSession now merges sessionState + context + store actions
- Wraps store actions to bind serverId automatically
- Updated prop types in components to use DaemonSession
- Fixed use-session-directory.ts to use SessionState

This restores component compatibility while keeping pure Zustand architecture.

Still TODO: Fix remaining components that use SessionContext directly

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-02 14:27:57 +00:00
parent f5ec3bffb7
commit e8aa08159a
8 changed files with 161 additions and 19 deletions

View File

@@ -171,7 +171,7 @@ export default function AgentScreen() {
}
type AgentScreenContentProps = {
session: SessionContextValue;
session: import("@/hooks/use-daemon-session").DaemonSession;
agentId?: string;
routeServerId: string;
onBack: () => void;

View File

@@ -88,7 +88,7 @@ export default function FileExplorerScreen() {
}
type FileExplorerContentProps = {
session: SessionContextValue;
session: import("@/hooks/use-daemon-session").DaemonSession;
agentId?: string;
pathParamRaw?: string | string[];
fileParamRaw?: string | string[];

View File

@@ -95,7 +95,7 @@ function GitDiffContent({
agentId,
serverLabel,
}: {
session: SessionContextValue;
session: import("@/hooks/use-daemon-session").DaemonSession;
agentId?: string;
serverLabel: string;
}) {

View File

@@ -1095,7 +1095,6 @@ const styles = StyleSheet.create(((theme: any) => ({
container: {
flexDirection: "column",
position: "relative",
flex: 1,
},
borderSeparator: {
height: theme.borderWidth[1],

View File

@@ -252,7 +252,7 @@ function AgentFlowModal({
}, [serverId, daemonEntries]);
const [selectedServerId, setSelectedServerId] = useState<string | null>(initialServerId);
const selectSessionSlice = useCallback(
(session: SessionContextValue | null): CreateAgentSessionSlice | null => {
(session: import("@/hooks/use-daemon-session").DaemonSession | null): CreateAgentSessionSlice | null => {
if (!session) {
return null;
}

View File

@@ -1,8 +1,8 @@
import { useCallback, useRef } from "react";
import { useCallback, useRef, useContext } from "react";
import { Alert } from "react-native";
import type { SessionContextValue } from "@/contexts/session-context";
import { SessionContext } from "@/contexts/session-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore } from "@/stores/session-store";
import { useSessionStore, type SessionState } from "@/stores/session-store";
export class DaemonSessionUnavailableError extends Error {
serverId: string;
@@ -19,14 +19,57 @@ type UseDaemonSessionOptions = {
allowUnavailable?: boolean;
};
// Combined type: SessionState (from store) + imperative APIs (from context)
export type DaemonSession = SessionState & {
// Imperative APIs from context
setVoiceDetectionFlags: (isDetecting: boolean, isSpeaking: boolean) => void;
requestGitDiff: (agentId: string) => void;
requestDirectoryListing: (agentId: string, path: string, options?: { recordHistory?: boolean }) => void;
requestFilePreview: (agentId: string, path: string) => void;
navigateExplorerBack: (agentId: string) => string | null;
requestProviderModels: (provider: any, options?: { cwd?: string }) => void;
restartServer: (reason?: string) => void;
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
cancelAgentRun: (agentId: string) => void;
sendAgentMessage: (
agentId: string,
message: string,
images?: Array<{ uri: string; mimeType?: string }>
) => Promise<void>;
sendAgentAudio: (
agentId: string,
audioBlob: Blob,
requestId?: string,
options?: { mode?: "transcribe_only" | "auto_run" }
) => Promise<void>;
deleteAgent: (agentId: string) => void;
createAgent: (options: {
config: any;
initialPrompt: string;
git?: any;
worktreeName?: string;
requestId?: string;
}) => void;
resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void;
setAgentMode: (agentId: string, modeId: string) => void;
respondToPermission: (agentId: string, requestId: string, response: any) => void;
// State getters/setters that components might use directly
getDraftInput: (agentId: string) => any;
saveDraftInput: (agentId: string, draft: any) => void;
setFocusedAgentId: (agentId: string | null) => void;
setMessages: (messages: any[] | ((prev: any[]) => any[])) => void;
setQueuedMessages: (value: any) => void;
};
export function useDaemonSession(
serverId?: string | null,
options?: UseDaemonSessionOptions & { allowUnavailable?: false }
): SessionContextValue | null;
): DaemonSession | null;
export function useDaemonSession(
serverId: string | null | undefined,
options: UseDaemonSessionOptions & { allowUnavailable: true }
): SessionContextValue | null;
): DaemonSession | null;
export function useDaemonSession(serverId?: string | null, options?: UseDaemonSessionOptions) {
const selectSession = useCallback(
(state: ReturnType<typeof useSessionStore.getState>) => {
@@ -37,21 +80,42 @@ export function useDaemonSession(serverId?: string | null, options?: UseDaemonSe
},
[serverId]
);
const session = useSessionStore(selectSession);
const sessionState = useSessionStore(selectSession);
const context = useContext(SessionContext);
const { connectionStates } = useDaemonConnections();
const alertedDaemonsRef = useRef<Set<string>>(new Set());
const loggedDaemonsRef = useRef<Set<string>>(new Set());
const { suppressUnavailableAlert = false, allowUnavailable = false } = options ?? {};
// Get store actions
const getDraftInput = useSessionStore((state) => state.getDraftInput);
const saveDraftInput = useSessionStore((state) => state.saveDraftInput);
const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId);
const setMessages = useSessionStore((state) => state.setMessages);
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
if (!serverId) {
return null;
}
try {
if (!session) {
if (!sessionState || !context) {
throw new DaemonSessionUnavailableError(serverId);
}
return session;
// Combine session state with imperative APIs from context and store actions
const combined: DaemonSession = {
...sessionState,
...context,
// Wrap store actions to bind serverId
getDraftInput: (agentId: string) => getDraftInput(serverId, agentId),
saveDraftInput: (agentId: string, draft: any) => saveDraftInput(serverId, agentId, draft),
setFocusedAgentId: (agentId: string | null) => setFocusedAgentId(serverId, agentId),
setMessages: (messages: any) => setMessages(serverId, messages),
setQueuedMessages: (value: any) => setQueuedMessages(serverId, value),
};
return combined;
} catch (error) {
if (error instanceof DaemonSessionUnavailableError) {
const connection = connectionStates.get(serverId);

View File

@@ -1,19 +1,19 @@
import { useCallback, useMemo } from "react";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useSessionStore, type SessionData } from "@/stores/session-store";
import { useSessionStore, type SessionState } from "@/stores/session-store";
export function useSessionDirectory(): Map<string, SessionData> {
export function useSessionDirectory(): Map<string, SessionState> {
const sessions = useSessionStore((state) => state.sessions);
return useMemo(() => {
return new Map<string, SessionData>(Object.entries(sessions));
return new Map<string, SessionState>(Object.entries(sessions));
}, [sessions]);
}
type SessionSelector<T> = (session: SessionData | null) => T;
type SessionSelector<T> = (session: SessionState | null) => T;
type EqualityFn<T> = ((left: T, right: T) => boolean) | undefined;
export function useSessionForServer(serverId: string | null): SessionData | null;
export function useSessionForServer(serverId: string | null): SessionState | null;
export function useSessionForServer<T>(
serverId: string | null,
selector: SessionSelector<T>,
@@ -23,7 +23,7 @@ export function useSessionForServer<T>(
serverId: string | null,
selector?: SessionSelector<T>,
equalityFn?: EqualityFn<T>
): SessionData | null | T {
): SessionState | null | T {
const baseSelector = useCallback(
(state: ReturnType<typeof useSessionStore.getState>) =>
(serverId ? state.sessions[serverId] ?? null : null),