{
if (!(event.target instanceof HTMLElement)) return;
lastFocusedSurfaceElement = event.target;
lastFocusedSurfaceAt = performance.now();
}}
onPasteCapture={handlePasteCapture}
>
{children}
);
};
/**
* The current react-lexical FocusPlugin focuses every mounted composer when a run starts. Mount
* this once above all surfaces so programmatic focus returns to the element/surface the user was
* actually interacting with after those listeners have run.
*/
export const AssistantRunFocusPreserver = () => {
useAuiEvent("thread.runStart", () => {
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
// Sending can briefly move focus to while Lexical reconciles the editor. Retain the
// last connected assistant input so the run still belongs to the surface the user submitted.
const recentlyFocusedSurface = performance.now() - lastFocusedSurfaceAt < 1000;
const focusedBeforeRun =
activeElement && activeElement !== document.body
? activeElement
: recentlyFocusedSurface && lastFocusedSurfaceElement?.isConnected
? lastFocusedSurfaceElement
: activeElement;
const surfaceIdBeforeRun = getSurfaceIdForElement(focusedBeforeRun);
const scrollPositionsBeforeRun = captureAssistantSurfaceScrollPositions();
let isRestoring = false;
const restoreFocus = () => {
if (isRestoring) return;
const surfaceIdAfterRun = getSurfaceIdForElement(document.activeElement);
const movedToAnotherSurface = surfaceIdAfterRun !== null && surfaceIdAfterRun !== surfaceIdBeforeRun;
const leftInitiatingSurface = surfaceIdBeforeRun !== null && surfaceIdAfterRun !== surfaceIdBeforeRun;
if (!movedToAnotherSurface && !leftInitiatingSurface) return;
isRestoring = true;
try {
if (focusedBeforeRun?.isConnected) focusedBeforeRun.focus({ preventScroll: true });
if (
getSurfaceIdForElement(document.activeElement) !== surfaceIdBeforeRun &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
// Focusing the other mounted Lexical composer may already have scrolled the dashboard or
// one of its nested board containers. Put every assistant surface ancestor back before the
// browser paints so a send from the floating panel never jumps to the board widget.
restoreAssistantSurfaceScrollPositions(scrollPositionsBeforeRun);
} finally {
isRestoring = false;
}
};
queueMicrotask(() => queueMicrotask(restoreFocus));
requestAnimationFrame(restoreFocus);
});
return null;
};
/**
* Gives every rendered assistant surface one shared local composer while preserving the shared
* conversation. Individual surfaces should add AssistantComposerSurfaceBoundary around their UI.
*/
export const AssistantComposerRuntimeProvider = ({ children }: PropsWithChildren) => {
const parentAui = useAui();
// oxlint-disable-next-line no-underscore-dangle -- required to derive a scoped runtime without cloning the shared thread.
const sharedRuntime = parentAui.threads().__internal_getAssistantRuntime?.();
const surfaceRuntime = useMemo(() => {
if (!sharedRuntime) throw new Error("Assistant runtime is not available for this composer surface");
return createAssistantSurfaceRuntime(sharedRuntime);
}, [sharedRuntime]);
useEffect(() => () => surfaceRuntime.dispose(), [surfaceRuntime]);
return (