Skip to content
Source
Sign inSign up

useProactivePanels.ts

211 lines7.6 KB

1import type { RunId, ScopedThreadRef, ThreadLinkedPullRequest } from "@t3tools/contracts";
2import { scopedThreadKey } from "@t3tools/client-runtime/environment";
3import { useEffect, useRef } from "react";
4import { useDiffPanelStore } from "../diffPanelStore";
5import {
6 selectActiveRightPanelSurface,
7 pullRequestSurface,
8 useRightPanelStore,
9} from "../rightPanelStore";
10import type { TurnDiffSummary } from "../types";
11import {
12 observeProactivePanelUserChoice,
13 resolveProactiveTurnDiffAction,
14 shouldOpenProactivePullRequest,
15 shouldOpenProactiveTurnDiff,
16 shouldRetargetThreadPullRequestPanel,
17} from "../components/ChatView.logic";
18​
19// Settings and drafts unmount ChatView; remember links until the page reloads.
20const observedThreadPullRequests = new Map<string, ThreadLinkedPullRequest | null>();
21​
22/**
23 * Opens the right panel on its own when there is something new to look at: a
24 * change request linked to the thread, or the diff of a turn that just
25 * completed. A panel the user opened or closed since wins; see
26 * observeProactivePanelUserChoice.
27 */
28export function useProactivePanels(input: {
29 // Null for drafts and while the thread is unavailable.
30 readonly threadRef: ScopedThreadRef | null;
31 readonly enabled: boolean;
32 readonly settingsHydrated: boolean;
33 readonly threadDetailLoading: boolean;
34 readonly runningTurnId: RunId | null;
35 readonly latestRun: { readonly runId: RunId; readonly status: string } | null;
36 readonly latestRunSettled: boolean;
37 readonly turnDiffSummaries: ReadonlyArray<TurnDiffSummary>;
38 readonly isGitRepo: boolean | undefined;
39 readonly linkedPullRequest: ThreadLinkedPullRequest | null;
40 readonly hasLinkedPullRequestDetail: boolean;
41 readonly pullRequestsKey: string | null;
42 readonly pullRequestsCapabilityKnown: boolean;
43 readonly supportsPullRequests: boolean;
44 readonly pullRequestsSurfaceAvailable: boolean;
45 readonly visiblePullRequestCount: number;
46 readonly onDiffPanelOpen: (() => void) | undefined;
47}) {
48 const {
49 threadRef,
50 enabled: proactivePanelsEnabledSetting,
51 settingsHydrated: clientSettingsHydrated,
52 threadDetailLoading,
53 runningTurnId: activeRunningTurnId,
54 latestRun: activeLatestRun,
55 latestRunSettled,
56 turnDiffSummaries,
57 isGitRepo,
58 linkedPullRequest: linkedThreadPullRequest,
59 hasLinkedPullRequestDetail,
60 pullRequestsKey: proactivePullRequestsKey,
61 pullRequestsCapabilityKnown,
62 supportsPullRequests,
63 pullRequestsSurfaceAvailable,
64 visiblePullRequestCount,
65 onDiffPanelOpen,
66 } = input;
67 const proactivePanelObservationRef = useRef<ReturnType<
68 typeof observeProactivePanelUserChoice
69 > | null>(null);
70 const threadKey = threadRef ? scopedThreadKey(threadRef) : null;
71​
72 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- opens proactive surfaces in the right panel store as runs and linked PRs change
73 useEffect(() => {
74 if (threadKey === null || threadRef === null) {
75 proactivePanelObservationRef.current = null;
76 return;
77 }
78 const panels = useRightPanelStore.getState();
79 const observation = observeProactivePanelUserChoice(proactivePanelObservationRef.current, {
80 threadKey: threadKey,
81 runningTurnId: activeRunningTurnId,
82 userActionRevision: panels.getUserActionRevision(threadRef),
83 });
84 proactivePanelObservationRef.current = observation;
85 const {
86 runningTurnId: previousRunningTurnId,
87 targetKey: previousTargetKey,
88 userActionRevision,
89 } = observation;
90 const openSurface = selectActiveRightPanelSurface(panels.byThreadKey, threadRef);
91 const previousPullRequest = observedThreadPullRequests.get(threadKey) ?? null;
92 // A missing link during loading must not erase the last observed one.
93 if (linkedThreadPullRequest !== null || !threadDetailLoading) {
94 observedThreadPullRequests.set(threadKey, linkedThreadPullRequest);
95 }
96 const followSelectedPullRequest = shouldRetargetThreadPullRequestPanel(
97 previousPullRequest,
98 linkedThreadPullRequest,
99 openSurface,
100 );
101 // Following the selected linked PR does not open an unrelated panel, so it
102 // remains available with proactive panels off. It still respects a later choice.
103 if (followSelectedPullRequest && linkedThreadPullRequest !== null) {
104 panels.openProactive(
105 threadRef,
106 pullRequestSurface(linkedThreadPullRequest),
107 userActionRevision,
108 true,
109 );
110 }
111 if (!clientSettingsHydrated) return;
112​
113 const proactivePanelsEnabled = proactivePanelsEnabledSetting;
114 const eligibleLink =
115 proactivePanelsEnabled &&
116 shouldOpenProactivePullRequest(previousTargetKey, proactivePullRequestsKey);
117 const shouldDeferLink = eligibleLink && !pullRequestsCapabilityKnown;
118 proactivePanelObservationRef.current = {
119 ...observation,
120 targetKey: shouldDeferLink ? (previousTargetKey ?? null) : proactivePullRequestsKey,
121 };
122 if (eligibleLink && pullRequestsCapabilityKnown) {
123 if (
124 !followSelectedPullRequest &&
125 pullRequestsSurfaceAvailable &&
126 (visiblePullRequestCount > 1 || !hasLinkedPullRequestDetail || !supportsPullRequests)
127 ) {
128 panels.openProactive(
129 threadRef,
130 { id: "pull-requests", kind: "pull-requests" },
131 userActionRevision,
132 );
133 } else if (
134 !followSelectedPullRequest &&
135 supportsPullRequests &&
136 linkedThreadPullRequest !== null
137 ) {
138 panels.openProactive(
139 threadRef,
140 pullRequestSurface(linkedThreadPullRequest),
141 userActionRevision,
142 );
143 }
144 }
145 if (threadDetailLoading) return;
146​
147 const settledTurnId = latestRunSettled ? (activeLatestRun?.runId ?? null) : null;
148 const newlyCompletedTurnId = shouldOpenProactiveTurnDiff({
149 previousRunningTurnId,
150 runningTurnId: activeRunningTurnId,
151 settledTurnId,
152 turnCompleted: activeLatestRun?.status === "completed",
153 })
154 ? settledTurnId
155 : null;
156 const eligibleCompletion =
157 proactivePanelsEnabled &&
158 newlyCompletedTurnId !== null &&
159 !(
160 proactivePullRequestsKey !== null &&
161 (!pullRequestsCapabilityKnown || supportsPullRequests || pullRequestsSurfaceAvailable)
162 );
163 const completedCheckpoint = eligibleCompletion
164 ? turnDiffSummaries.find((checkpoint) => checkpoint.runId === newlyCompletedTurnId)
165 : undefined;
166 const diffAction = eligibleCompletion
167 ? resolveProactiveTurnDiffAction({
168 checkpoint: completedCheckpoint,
169 isGitRepo,
170 })
171 : "ignore";
172 proactivePanelObservationRef.current = {
173 ...proactivePanelObservationRef.current,
174 // Preserve first-entry eligibility while capabilities, checkpoint or repository load.
175 runningTurnId:
176 diffAction === "defer" || shouldDeferLink ? previousRunningTurnId : activeRunningTurnId,
177 };
178 if (diffAction !== "open" || newlyCompletedTurnId === null) return;
179 if (
180 !panels.openProactive(
181 threadRef,
182 { kind: "diff", turnId: newlyCompletedTurnId },
183 userActionRevision,
184 )
185 ) {
186 return;
187 }
188 useDiffPanelStore.getState().selectGitScope(threadRef, "branch");
189 onDiffPanelOpen?.();
190 }, [
191 activeLatestRun?.runId,
192 activeLatestRun?.status,
193 activeRunningTurnId,
194 clientSettingsHydrated,
195 hasLinkedPullRequestDetail,
196 isGitRepo,
197 latestRunSettled,
198 linkedThreadPullRequest,
199 onDiffPanelOpen,
200 proactivePanelsEnabledSetting,
201 proactivePullRequestsKey,
202 pullRequestsCapabilityKnown,
203 pullRequestsSurfaceAvailable,
204 supportsPullRequests,
205 threadDetailLoading,
206 threadKey,
207 threadRef,
208 turnDiffSummaries,
209 visiblePullRequestCount,
210 ]);