import { useRef, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { AppState, Platform } from "react-native";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDaemonClient } from "@/hooks/use-daemon-client";
import { useAudioPlayer } from "@/hooks/use-audio-player";
import { useClientActivity } from "@/hooks/use-client-activity";
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
import {
applyStreamEvent,
generateMessageId,
hydrateStreamState,
reduceStreamUpdate,
type StreamItem,
} from "@/types/stream";
import type {
ActivityLogPayload,
AgentSnapshotPayload,
AgentStreamEventPayload,
SessionOutboundMessage,
} from "@server/shared/messages";
import { parseServerInfoStatusPayload } from "@server/shared/messages";
import {
buildAgentAttentionNotificationPayload,
type AgentAttentionNotificationPayload,
type NotificationPermissionRequest,
} from "@server/shared/agent-attention-notification";
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { DaemonClient, ConnectionState } from "@server/client/daemon-client";
import { File } from "expo-file-system";
import { useDaemonConnections } from "./daemon-connections-context";
import {
useSessionStore,
type Agent,
type SessionState,
} from "@/stores/session-store";
import { useDraftStore } from "@/stores/draft-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory";
import { sendOsNotification } from "@/utils/os-notifications";
import { getInitKey, getInitDeferred, resolveInitDeferred, rejectInitDeferred, createInitDeferred } from "@/utils/agent-initialization";
import { encodeImages } from "@/utils/encode-images";
// Re-export types from session-store and draft-store for backward compatibility
export type { DraftInput } from "@/stores/draft-store";
export type {
MessageEntry,
Agent,
ExplorerEntry,
ExplorerFile,
ExplorerEntryKind,
ExplorerFileKind,
ExplorerEncoding,
AgentFileExplorerState,
} from "@/stores/session-store";
const derivePendingPermissionKey = (
agentId: string,
request: NotificationPermissionRequest
) => {
const fallbackId =
request.id ||
(typeof request.metadata?.id === "string"
? request.metadata.id
: undefined) ||
request.name ||
request.title ||
`${request.kind}:${JSON.stringify(
request.input ?? request.metadata ?? {}
)}`;
return `${agentId}:${fallbackId}`;
};
const HISTORY_STALE_AFTER_MS = 60_000;
type SessionConnectionSnapshot = {
isConnected: boolean;
isConnecting: boolean;
lastError: string | null;
};
const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
for (let i = items.length - 1; i >= 0; i -= 1) {
const item = items[i];
if (item.kind === "assistant_message") {
return item.text;
}
}
return null;
};
const mapConnectionState = (
state: ConnectionState,
lastError: string | null
): SessionConnectionSnapshot => ({
isConnected: state.status === "connected",
isConnecting: state.status === "connecting",
lastError: state.status === "disconnected" ? state.reason ?? lastError : null,
});
const getLatestPermissionRequest = (
session: SessionState | undefined,
agentId: string
): NotificationPermissionRequest | null => {
if (!session) {
return null;
}
let latest: NotificationPermissionRequest | null = null;
for (const pending of session.pendingPermissions.values()) {
if (pending.agentId === agentId) {
latest = pending.request;
}
}
if (latest) {
return latest;
}
const agentPending = session.agents.get(agentId)?.pendingPermissions;
if (agentPending && agentPending.length > 0) {
return agentPending[agentPending.length - 1] as NotificationPermissionRequest;
}
return null;
};
type FileExplorerPayload = Extract<
SessionOutboundMessage,
{ type: "file_explorer_response" }
>["payload"];
type FileDownloadTokenPayload = Extract<
SessionOutboundMessage,
{ type: "file_download_token_response" }
>["payload"];
type AgentUpdatePayload = Extract<
SessionOutboundMessage,
{ type: "agent_update" }
>["payload"];
const getAgentIdFromUpdate = (update: AgentUpdatePayload): string =>
update.kind === "remove" ? update.agentId : update.agent.id;
function normalizeAgentSnapshot(
snapshot: AgentSnapshotPayload,
serverId: string
) {
const createdAt = new Date(snapshot.createdAt);
const updatedAt = new Date(snapshot.updatedAt);
const lastUserMessageAt = snapshot.lastUserMessageAt
? new Date(snapshot.lastUserMessageAt)
: null;
const attentionTimestamp = snapshot.attentionTimestamp
? new Date(snapshot.attentionTimestamp)
: null;
const archivedAt = snapshot.archivedAt
? new Date(snapshot.archivedAt)
: null;
return {
serverId,
id: snapshot.id,
provider: snapshot.provider,
status: snapshot.status as AgentLifecycleStatus,
createdAt,
updatedAt,
lastUserMessageAt,
lastActivityAt: updatedAt,
capabilities: snapshot.capabilities,
currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes ?? [],
pendingPermissions: snapshot.pendingPermissions ?? [],
persistence: snapshot.persistence ?? null,
runtimeInfo: snapshot.runtimeInfo,
lastUsage: snapshot.lastUsage,
lastError: snapshot.lastError ?? null,
title: snapshot.title ?? null,
cwd: snapshot.cwd,
model: snapshot.model ?? null,
thinkingOptionId: snapshot.thinkingOptionId ?? null,
requiresAttention: snapshot.requiresAttention ?? false,
attentionReason: snapshot.attentionReason ?? null,
attentionTimestamp,
archivedAt,
labels: snapshot.labels,
projectPlacement: null,
};
}
const createExplorerState = () => ({
directories: new Map(),
files: new Map(),
isLoading: false,
lastError: null,
pendingRequest: null,
currentPath: ".",
history: ["."],
lastVisitedPath: ".",
selectedEntryPath: null,
});
const pushHistory = (history: string[], path: string): string[] => {
const normalizedHistory = history.length === 0 ? ["."] : history;
const last = normalizedHistory[normalizedHistory.length - 1];
if (last === path) {
return normalizedHistory;
}
return [...normalizedHistory, path];
};
interface SessionProviderSharedProps {
children: ReactNode;
serverId: string;
}
interface SessionProviderUrlProps extends SessionProviderSharedProps {
serverUrl: string;
daemonPublicKeyB64?: string;
}
interface SessionProviderClientProps extends SessionProviderSharedProps {
client: DaemonClient;
}
export type SessionProviderProps = SessionProviderUrlProps | SessionProviderClientProps;
function SessionProviderWithUrl({
children,
serverId,
serverUrl,
daemonPublicKeyB64,
}: SessionProviderUrlProps) {
const client = useDaemonClient(serverUrl, { daemonPublicKeyB64 });
return (
);
}
function SessionProviderWithClient({
children,
serverId,
client,
}: SessionProviderClientProps) {
return (
);
}
// SessionProvider: Daemon client message handler that updates Zustand store
export function SessionProvider(props: SessionProviderProps) {
if ("client" in props) {
return ;
}
return ;
}
function SessionProviderInternal({
children,
serverId,
client,
}: SessionProviderClientProps) {
const queryClient = useQueryClient();
const [connectionSnapshot, setConnectionSnapshot] =
useState(() =>
mapConnectionState(client.getConnectionState(), client.lastError)
);
const { markAgentListReady } = useDaemonConnections();
// Zustand store actions
const initializeSession = useSessionStore((state) => state.initializeSession);
const clearSession = useSessionStore((state) => state.clearSession);
const setIsPlayingAudio = useSessionStore((state) => state.setIsPlayingAudio);
const setMessages = useSessionStore((state) => state.setMessages);
const setCurrentAssistantMessage = useSessionStore(
(state) => state.setCurrentAssistantMessage
);
const setAgentStreamTail = useSessionStore(
(state) => state.setAgentStreamTail
);
const setAgentStreamHead = useSessionStore(
(state) => state.setAgentStreamHead
);
const clearAgentStreamHead = useSessionStore(
(state) => state.clearAgentStreamHead
);
const setAgentTimelineCursor = useSessionStore(
(state) => state.setAgentTimelineCursor
);
const setInitializingAgents = useSessionStore(
(state) => state.setInitializingAgents
);
const bumpHistorySyncGeneration = useSessionStore(
(state) => state.bumpHistorySyncGeneration
);
const markAgentHistorySynchronized = useSessionStore(
(state) => state.markAgentHistorySynchronized
);
const setHasHydratedAgents = useSessionStore(
(state) => state.setHasHydratedAgents
);
const setAgents = useSessionStore((state) => state.setAgents);
const setAgentLastActivity = useSessionStore(
(state) => state.setAgentLastActivity
);
const setPendingPermissions = useSessionStore(
(state) => state.setPendingPermissions
);
const setFileExplorer = useSessionStore((state) => state.setFileExplorer);
const clearDraftInput = useDraftStore((state) => state.clearDraftInput);
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
const updateSessionClient = useSessionStore((state) => state.updateSessionClient);
const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo);
// Track focused agent for heartbeat
const focusedAgentId = useSessionStore(
(state) => state.sessions[serverId]?.focusedAgentId ?? null
);
const handleAppResumed = useCallback(
(awayMs: number) => {
if (awayMs < HISTORY_STALE_AFTER_MS) {
return;
}
bumpHistorySyncGeneration(serverId);
},
[bumpHistorySyncGeneration, serverId]
);
// Client activity tracking (heartbeat, push token registration)
useClientActivity({ client, focusedAgentId, onAppResumed: handleAppResumed });
usePushTokenRegistration({ client, serverId });
// State for voice detection flags (will be set by RealtimeContext)
const isDetectingRef = useRef(false);
const isSpeakingRef = useRef(false);
const audioPlayer = useAudioPlayer({
isDetecting: () => isDetectingRef.current,
isSpeaking: () => isSpeakingRef.current,
});
const activeAudioGroupsRef = useRef>(new Set());
const previousAgentStatusRef = useRef