File size: 4,722 Bytes
5f40163 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import React from "react";
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { useSubConversations } from "#/hooks/query/use-sub-conversations";
import type { AppConversation } from "#/api/conversation-service/agent-server-conversation-service.types";
import { useConversationStore } from "#/stores/conversation-store";
import { useActiveBackend } from "#/contexts/active-backend-context";
import { findPlannerConversationId } from "#/utils/plan-file";
interface WebSocketProviderWrapperProps {
children: React.ReactNode;
conversationId: string;
}
export function WebSocketProviderWrapper({
children,
conversationId,
}: WebSocketProviderWrapperProps) {
const { data: conversation } = useActiveConversation();
const { backend } = useActiveBackend();
const isLocalBackend = backend.kind !== "cloud";
const localPlanningConversationId = useConversationStore(
(state) => state.localPlanningConversationId,
);
// `localPlanningConversationId` is a single unscoped Zustand field. Right
// after switching conversations it can still hold the *previous*
// conversation's planner id, since the reset effect in
// routes/conversation.tsx and `useActiveConversation()`'s refetch both lag
// this render — only trust it once `conversation` matches `conversationId`.
const isConversationDataFresh = conversation?.id === conversationId;
const trustedLocalPlanningConversationId = isConversationDataFresh
? localPlanningConversationId
: null;
// Candidate ids to resolve. On local backends `sub_conversation_ids` is the
// generic (untyped) child list, so fetch it below to find the
// `plannerparent`-tagged entry; cloud has no such ambiguity. Memoized: a
// fresh array literal each render would re-fire ConversationWebSocketProvider's
// reference-keyed effects and wipe the pending PLAN.md read.
const candidateConversationIds = React.useMemo(() => {
if (!isLocalBackend) {
return conversation?.sub_conversation_ids ?? [];
}
if (
conversation?.sub_conversation_ids &&
conversation.sub_conversation_ids.length > 0
) {
return conversation.sub_conversation_ids;
}
return trustedLocalPlanningConversationId
? [trustedLocalPlanningConversationId]
: [];
}, [
isLocalBackend,
conversation?.sub_conversation_ids,
trustedLocalPlanningConversationId,
]);
const { data: subConversations } = useSubConversations(
candidateConversationIds,
);
// Identify the planner via the `plannerparent` tag rather than list
// position — an unrelated child must never be adopted as the planner.
// Kept as its own memo (a primitive) rather than inlined below: `subConversations`
// gets a new array reference on every refetch even when the planner id
// hasn't changed, and planningConversationIds must not rebuild its own
// array in that case — see candidateConversationIds above.
const plannerConversationId = React.useMemo(() => {
if (!isLocalBackend) return null;
return findPlannerConversationId(subConversations, conversation?.id);
}, [isLocalBackend, subConversations, conversation?.id]);
const planningConversationIds = React.useMemo(() => {
if (!isLocalBackend) return candidateConversationIds;
if (plannerConversationId) return [plannerConversationId];
// Tag data hasn't resolved yet — bridge with the verified store id (see
// `trustedLocalPlanningConversationId` above), otherwise stay empty
// rather than guessing an untagged child is the planner.
return trustedLocalPlanningConversationId
? [trustedLocalPlanningConversationId]
: [];
}, [
isLocalBackend,
candidateConversationIds,
plannerConversationId,
trustedLocalPlanningConversationId,
]);
const filteredSubConversations = subConversations?.filter(
(subConversation): subConversation is AppConversation =>
subConversation !== null &&
planningConversationIds.includes(subConversation.id),
);
// Suppress the URL while the cloud sandbox is PAUSED — it still points at
// the old (now-rejecting) host until the sandbox resumes.
const conversationUrl =
conversation?.sandbox_status === "PAUSED"
? null
: conversation?.conversation_url;
return (
<ConversationWebSocketProvider
conversationId={conversationId}
conversationUrl={conversationUrl}
sessionApiKey={conversation?.session_api_key}
subConversationIds={planningConversationIds}
subConversations={filteredSubConversations}
>
{children}
</ConversationWebSocketProvider>
);
}
|