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>
  );
}