Skip to content

Commit 9c9d29d

Browse files
Restore mid-trickle acceleration when the canonical action lands
Co-authored-by: Marco Chávez <marcochavezf@users.noreply.github.com>
1 parent c4ff7b6 commit 9c9d29d

1 file changed

Lines changed: 45 additions & 5 deletions

File tree

src/hooks/usePendingConciergeResponse.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {clearAgentZeroProcessingIndicator} from '@libs/actions/Report';
22
import {applyPendingConciergeAction, clearPendingFollowupList, discardPendingConciergeAction, hidePendingFollowupList} from '@libs/actions/Report/SuggestedFollowup';
33
import AgentZeroOptimisticStore, {MAX_AGE_MS} from '@libs/AgentZeroOptimisticStore';
4-
import {getOptimisticRevealDurationMS, MIN_TRICKLE_TOKEN_COUNT, TICK_INTERVAL_MS, TRICKLE_HARD_CAP_MS} from '@libs/ConciergeRevealUtils';
4+
import {ACCELERATED_REMAINING_MS, getOptimisticRevealDurationMS, MIN_TRICKLE_TOKEN_COUNT, TICK_INTERVAL_MS, TRICKLE_HARD_CAP_MS} from '@libs/ConciergeRevealUtils';
55
import Log from '@libs/Log';
66
import {rand64} from '@libs/NumberUtils';
77
import type {ConciergeDraftEvent} from '@libs/Pusher/types';
@@ -46,6 +46,7 @@ function usePendingConciergeResponse(reportID: string | undefined) {
4646
const {dispatchLocalDraftEvent} = useConciergeDraftActions();
4747

4848
const tokens = tokenizeForReveal(fullHtml);
49+
const accelerateRef = useRef<((nowMs: number) => void) | null>(null);
4950

5051
// Captured into a ref so the trickle effect can re-run only on the IDs that
5152
// identify a distinct Concierge reply. Composer typing, unrelated Onyx emits,
@@ -58,6 +59,16 @@ function usePendingConciergeResponse(reportID: string | undefined) {
5859
trickleInputsRef.current = {pendingResponse, fullHtml, tokens, dispatchLocalDraftEvent, persistedAction};
5960
});
6061

62+
// Reconciliation: when the canonical reportComment lands in REPORT_ACTIONS
63+
// mid-trickle, fire the running loop's accelerator so the remaining reveal
64+
// finishes in ~1.5s instead of snapping the synthetic bubble closed.
65+
useEffect(() => {
66+
if (!persistedAction || !accelerateRef.current) {
67+
return;
68+
}
69+
accelerateRef.current(Date.now());
70+
}, [persistedAction]);
71+
6172
const lastOnlineTransitionAtRef = useRef<number>(0);
6273
const wasOfflineRef = useRef<boolean>(isOffline);
6374
useEffect(() => {
@@ -146,10 +157,16 @@ function usePendingConciergeResponse(reportID: string | undefined) {
146157
let sequence = 0;
147158
let intervalID: ReturnType<typeof setInterval> | null = null;
148159
let trickleStart = 0;
149-
const effectiveDuration = getOptimisticRevealDurationMS(snapshotTokens.length);
160+
// The accelerator recomputes effectiveDuration when the canonical reply lands
161+
// so the tail finishes quickly.
162+
let effectiveDuration = getOptimisticRevealDurationMS(snapshotTokens.length);
150163
let lastStage = 0;
151164
let cancelled = false;
152165
const clampProgress = (elapsedMs: number) => Math.max(0, Math.min(1, elapsedMs / effectiveDuration));
166+
// Snapshot of trickle progress at the moment the canonical reportComment
167+
// arrives. Presence (`arrival !== undefined`) doubles as the
168+
// "acceleration fired" check that selects the completion reason below.
169+
let arrival: {progress: number; elapsedMs: number} | undefined;
153170

154171
const dispatch = (status: ConciergeDraftEvent['status'], finalRenderedHTML: string) => {
155172
if (cancelled) {
@@ -175,24 +192,46 @@ function usePendingConciergeResponse(reportID: string | undefined) {
175192
intervalID = null;
176193
}
177194
const totalElapsedMs = trickleStart === 0 ? 0 : Date.now() - trickleStart;
178-
const reason: 'natural' | 'stale_cap' = totalElapsedMs >= TRICKLE_HARD_CAP_MS ? 'stale_cap' : 'natural';
195+
let reason: 'natural' | 'accelerated' | 'stale_cap' = 'natural';
196+
if (arrival) {
197+
reason = 'accelerated';
198+
} else if (totalElapsedMs >= TRICKLE_HARD_CAP_MS) {
199+
reason = 'stale_cap';
200+
}
179201
Log.info('[ConciergeTrickle] complete', false, {
180202
reportActionID,
181203
reason,
182204
tokenCount: snapshotTokens.length,
183205
durationMs: effectiveDuration,
184206
totalElapsedMs,
207+
arrivedAtProgress: arrival?.progress,
208+
arrivedAtElapsedMs: arrival?.elapsedMs,
185209
});
186210
dispatch('completed', snapshotTokens.at(-1) ?? snapshotHtml);
187211
// Don't reapply our older optimistic when the canonical is already there —
188-
// Reapplying it would overwrite server-added follow-up buttons and deep-link Pressables.
189-
if (trickleInputsRef.current.persistedAction) {
212+
// it would clobber server-added markup (follow-up buttons, deep-link
213+
// Pressables). `arrival` covers the accelerator path; the live ref read
214+
// catches arrivals during the pre-trickle setTimeout where the accelerator
215+
// no-ops on null intervalID.
216+
if (arrival || trickleInputsRef.current.persistedAction) {
190217
discardPendingConciergeAction(reportID);
191218
} else {
192219
applyPendingConciergeAction(reportID, reportAction);
193220
}
194221
};
195222

223+
accelerateRef.current = (nowMs: number) => {
224+
if (!intervalID || trickleStart === 0) {
225+
return;
226+
}
227+
const elapsed = nowMs - trickleStart;
228+
// Compressing effectiveDuration is what makes progress hit 1 within
229+
// ACCELERATED_REMAINING_MS — the next tick observes progress >= 1
230+
// and runs completeAndApply via the normal path.
231+
arrival = {progress: clampProgress(elapsed), elapsedMs: elapsed};
232+
effectiveDuration = elapsed + ACCELERATED_REMAINING_MS;
233+
};
234+
196235
const startTrickle = () => {
197236
if (cancelled) {
198237
return;
@@ -242,6 +281,7 @@ function usePendingConciergeResponse(reportID: string | undefined) {
242281
if (intervalID) {
243282
clearInterval(intervalID);
244283
}
284+
accelerateRef.current = null;
245285
};
246286
}, [reportID, reportActionID]);
247287
}

0 commit comments

Comments
 (0)