+
{
+ if (!(event.target instanceof HTMLElement)) return;
+ lastFocusedSurfaceElement = event.target;
+ lastFocusedSurfaceAt = performance.now();
+ }}
+ onPasteCapture={handlePasteCapture}
+ >
{children}
@@ -476,19 +521,42 @@ const AssistantComposerSurfaceBoundary = ({ children, surfaceId }: PropsWithChil
*/
export const AssistantRunFocusPreserver = () => {
useAuiEvent("thread.runStart", () => {
- const focusedBeforeRun = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+ 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);
- if (!surfaceIdAfterRun || surfaceIdAfterRun === surfaceIdBeforeRun) return;
+ const movedToAnotherSurface = surfaceIdAfterRun !== null && surfaceIdAfterRun !== surfaceIdBeforeRun;
+ const leftInitiatingSurface = surfaceIdBeforeRun !== null && surfaceIdAfterRun !== surfaceIdBeforeRun;
+ if (!movedToAnotherSurface && !leftInitiatingSurface) return;
- if (focusedBeforeRun?.isConnected) focusedBeforeRun.focus({ preventScroll: true });
- if (
- getSurfaceIdForElement(document.activeElement) !== surfaceIdBeforeRun &&
- document.activeElement instanceof HTMLElement
- ) {
- document.activeElement.blur();
+ 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;
}
};
diff --git a/apps/nextjs/src/components/assistant/assistant-widget.tsx b/apps/nextjs/src/components/assistant/assistant-widget.tsx
index 898023337..e00ee137f 100644
--- a/apps/nextjs/src/components/assistant/assistant-widget.tsx
+++ b/apps/nextjs/src/components/assistant/assistant-widget.tsx
@@ -1,6 +1,6 @@
"use client";
-import type { ReactNode } from "react";
+import type { CSSProperties, ReactNode } from "react";
import { useCallback, useState } from "react";
import { useAui, useAuiState } from "@assistant-ui/react";
import { ActionIcon, Box, Button, Center, Group, Loader, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core";
@@ -8,6 +8,7 @@ import { IconAlertTriangle, IconArrowsMaximize, IconMessage, IconRobot } from "@
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "@homarr/widgets";
+import { useRequiredBoard } from "@homarr/boards/context";
import classes from "./assistant-panel.module.css";
import { AssistantConversationSurface } from "./assistant-panel";
@@ -31,6 +32,7 @@ export const AssistantBoardWidget = (props: WidgetComponentProps<"assistant">) =
const EnabledAssistantBoardWidget = ({ options, width, height, isEditMode }: WidgetComponentProps<"assistant">) => {
const t = useI18n();
+ const board = useRequiredBoard();
const assistant = useHomarrAssistant();
const preferences = useAssistantPreferences();
const aui = useAui();
@@ -110,9 +112,11 @@ const EnabledAssistantBoardWidget = ({ options, width, height, isEditMode }: Wid
return (