Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions src/web-ui/src/flow_chat/components/modern/usePermissionRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ const EMPTY_EXTERNAL_REQUESTS: PermissionRequest[] = [];
const noopSubscribe = () => () => {};
const emptyExternalSnapshot = () => EMPTY_EXTERNAL_REQUESTS;

// Fine-grained, zero-argument snapshot projections of the live mailbox.
// `useSyncExternalStore` calls `getSnapshot` with no arguments and compares the
// result with `Object.is`, so each derivation must return the store's own
// already-immutable slice (no in-place mutation, no local identity caching that
// could swallow a nested mutation). Returning the field directly means the
// `Object.is` bail-out happens per slice, so churn in unrelated snapshot fields
// (pending replies, transport state, other mailboxes) no longer re-renders the
// consumers of this hook.
const getLiveSurfaceId = (): string =>
liveSessionInteractionStore.getActiveSnapshot().surfaceId;

const getLiveRequests = (): PermissionRequest[] =>
liveSessionInteractionStore.getActiveSnapshot().requests;

export function usePermissionRequests(sessionId?: string) {
// Driver resolution is per-render: the caller re-renders on any session
// change, so a projection whose dispatch config binds late is picked up.
Expand All @@ -38,24 +52,35 @@ export function usePermissionRequests(sessionId?: string) {
isLiveSource ? emptyExternalSnapshot : source.getSnapshot,
) as unknown as PermissionRequest[];

const liveMailbox = useSyncExternalStore(
// Two independent, fine-grained subscriptions replace the previous coarse
// whole-snapshot subscription. Each getter is a proper zero-argument
// `getSnapshot` that returns the store's own slice, so React bails out with
// `Object.is` unless that specific slice changed. The subscription itself is
// the store's own stable `subscribe` (module-level wrapper is unnecessary).
const liveSurfaceId = useSyncExternalStore(
liveSessionInteractionStore.subscribe,
getLiveSurfaceId,
getLiveSurfaceId,
);

const liveRequests = useSyncExternalStore(
liveSessionInteractionStore.subscribe,
liveSessionInteractionStore.getActiveSnapshot,
liveSessionInteractionStore.getActiveSnapshot,
getLiveRequests,
getLiveRequests,
);

useEffect(() => {
if (isLiveSource) void ensureActivePermissionMailbox();
}, [isLiveSource, liveMailbox.surfaceId]);
}, [isLiveSource, liveSurfaceId]);

const respond = useCallback(
async (requestId: string, reply: PermissionReplyKind, feedback?: string) => {
await driver.respondPermission(sessionId ?? '', requestId, reply, feedback);
if (isLiveSource) {
liveSessionInteractionStore.markPermissionResolved(liveMailbox.surfaceId, requestId);
liveSessionInteractionStore.markPermissionResolved(liveSurfaceId, requestId);
}
},
[driver, sessionId, isLiveSource, liveMailbox.surfaceId],
[driver, sessionId, isLiveSource, liveSurfaceId],
);

const respondBatch = useCallback(
Expand All @@ -68,17 +93,14 @@ export function usePermissionRequests(sessionId?: string) {
);
if (isLiveSource) {
resolvedRequestIds.forEach((requestId) => {
liveSessionInteractionStore.markPermissionResolved(
liveMailbox.surfaceId,
requestId,
);
liveSessionInteractionStore.markPermissionResolved(liveSurfaceId, requestId);
});
}
},
[driver, sessionId, isLiveSource, liveMailbox.surfaceId],
[driver, sessionId, isLiveSource, liveSurfaceId],
);

const effectiveRequests = isLiveSource ? liveMailbox.requests : externalRequests;
const effectiveRequests = isLiveSource ? liveRequests : externalRequests;
const sessionRequests = useMemo(
() => selectPermissionRequestsForSession(effectiveRequests, sessionId),
[effectiveRequests, sessionId],
Expand Down
Loading