diff --git a/apps/docs/docs/widgets/assistant/index.mdx b/apps/docs/docs/widgets/assistant/index.mdx
index 2a4e22645..9454c6e1f 100644
--- a/apps/docs/docs/widgets/assistant/index.mdx
+++ b/apps/docs/docs/widgets/assistant/index.mdx
@@ -5,7 +5,6 @@ hide_title: true
---
import { AddingWidget } from "@site/src/components/widgets/adding";
-import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header";
import { assistantWidget } from ".";
@@ -28,15 +27,8 @@ touch devices. When a board layout does not provide enough height for conversati
a compact **Open assistant** action instead of squeezing an unusable composer into the card. Increase its height
in that layout to keep the complete conversation inline.
-### Configuration
-
-
-
-**Current conversation** follows whichever conversation is active in the user-menu panel, `Shift + A`, or
-the command menu. This is the default and keeps every assistant entry point synchronized.
-
-**Pinned conversation** reveals a searchable conversation selector. A pinned widget never changes the
-active conversation silently: select **Open conversation** inside the widget before continuing that thread.
+The widget follows whichever conversation is active in the user-menu panel, `Shift + A`, or the command menu. This
+keeps every assistant entry point synchronized without storing a user's private conversation details in board settings.
### Conversation features
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
index ddb379fae..66c3819c1 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
@@ -44,9 +44,11 @@ export const createBoardContentPage = >(
page: async ({ params }: { params: Promise }) => {
const resolvedParams = await params;
const queryClient = getQueryClient();
- const session = await auth();
+ const sessionPromise = auth();
+ const boardPromise = getInitialBoard(resolvedParams);
+ const session = await sessionPromise;
- const board = await getInitialBoard(resolvedParams).catch((error) => {
+ const board = await boardPromise.catch((error) => {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (!session) {
logger.debug("No home board found for anonymous user, redirecting to login");
diff --git a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx
index 5f75c9227..e1a2fbe75 100644
--- a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx
@@ -2,6 +2,7 @@
import type { ReactNode } from "react";
import { useEffect, useMemo, useState } from "react";
+import { useRouter } from "next/navigation";
import {
Accordion,
ActionIcon,
@@ -140,6 +141,7 @@ const ConfigurationSkeleton = ({ label }: { label: string }) => (
export const AssistantConfiguration = () => {
const t = useScopedI18n("management.page.settings.section.assistant");
+ const router = useRouter();
const utils = clientApi.useUtils();
const { data: configuration, isLoading } = clientApi.assistant.getAdminConfiguration.useQuery();
const [provider, setProvider] = useState("openrouter");
@@ -268,6 +270,7 @@ export const AssistantConfiguration = () => {
utils.assistant.getAvailability.invalidate(),
utils.assistant.getRuntimeOptions.invalidate(),
]);
+ router.refresh();
showSuccessNotification({
title: t("notification.saved.title"),
message: t("notification.saved.message"),
diff --git a/apps/nextjs/src/components/assistant/assistant-panel.tsx b/apps/nextjs/src/components/assistant/assistant-panel.tsx
index 5a34c70ab..9ee5cfafa 100644
--- a/apps/nextjs/src/components/assistant/assistant-panel.tsx
+++ b/apps/nextjs/src/components/assistant/assistant-panel.tsx
@@ -159,6 +159,7 @@ import { getAssistantIconSearchQuery } from "./assistant-tool-label";
import { getAssistantProviderQuotaLevel, isAssistantProviderUnavailable } from "./assistant-provider-quota";
import { getAssistantToolTraceTarget } from "./assistant-tool-trace";
import { getSafeAssistantHttpUrl } from "./assistant-url";
+import { isEscapeOwnedByNestedOverlay } from "../board/advanced-focus/escape";
// Shiki's web grammar bundle is intentionally loaded only when a settled response contains a code
// block. Most assistant conversations never need it, so it should not inflate the global dashboard
@@ -407,7 +408,27 @@ type MarkdownSpanProps = ComponentPropsWithoutRef<"span"> & {
"data-directive-type"?: string;
};
-const AssistantDirectiveEntitiesContext = createContext([]);
+interface AssistantDirectiveEntitiesState {
+ entities: AssistantDirectiveEntity[];
+ isLoading: boolean;
+}
+
+const AssistantDirectiveEntitiesContext = createContext({
+ entities: [],
+ isLoading: false,
+});
+
+const AssistantDirectiveEntitiesProvider = ({ children }: { children: ReactNode }) => {
+ const { data: entities = [], isLoading } = clientApi.assistant.getContextEntities.useQuery(undefined, {
+ staleTime: 60_000,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
const AssistantMarkdownSpan = ({
children,
@@ -417,7 +438,7 @@ const AssistantMarkdownSpan = ({
"data-directive-type": directiveType,
...props
}: MarkdownSpanProps) => {
- const entities = useContext(AssistantDirectiveEntitiesContext);
+ const { entities } = useContext(AssistantDirectiveEntitiesContext);
if (assistantDirective === "true" && directiveId && directiveLabel && directiveType) {
const entity = resolveAssistantDirectiveEntity(entities, {
id: directiveId,
@@ -444,26 +465,20 @@ const assistantMarkdownComponents = unstable_memoizeMarkdownComponents({
SyntaxHighlighter: AssistantSyntaxHighlighter,
});
-const AssistantTextPart = () => {
- const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
-
- return (
-
-
-
- );
-};
+const AssistantTextPart = () => (
+
+);
const UserTextPart = () => {
const { text } = useMessagePartText();
- const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
+ const { entities } = useContext(AssistantDirectiveEntitiesContext);
const segments = parseAssistantDirectives(text);
return (
@@ -548,7 +563,7 @@ const SentAttachment = () => ;
const HiddenMessagePart = () => null;
const ReasoningPart = (_props: ReasoningMessagePartProps) => (
-
+
{
const aui = useAui();
const mentionViewportRef = useRef(null);
const slashViewportRef = useRef(null);
- const { data: entities = [], isLoading } = clientApi.assistant.getContextEntities.useQuery(undefined, {
- staleTime: 60_000,
- });
+ const { entities, isLoading } = useContext(AssistantDirectiveEntitiesContext);
const categories = useMemo(
() =>
(["app", "integration", "board", "widget"] as const).map((type) => ({
@@ -2914,7 +2927,7 @@ const AssistantActivityBar = ({
return (
-
+
@@ -2996,7 +3009,7 @@ export const AssistantConversationSurface = ({
const scrollToLatestBehavior = isRunning || reducedMotion ? "instant" : "smooth";
return (
- <>
+
{onWindowClose && onMinimize && (
- >
+
);
};
@@ -3135,9 +3148,19 @@ export const AssistantPanel = ({
}: AssistantPanelProps) => {
const t = useScopedI18n("common.assistant");
const previousFocusRef = useRef(null);
+ const panelRef = useRef(null);
useWindowEvent("keydown", (event) => {
- if (opened && event.key === "Escape") onClose();
+ if (
+ !opened ||
+ event.key !== "Escape" ||
+ event.defaultPrevented ||
+ event.isComposing ||
+ isEscapeOwnedByNestedOverlay(event.target, panelRef.current)
+ )
+ return;
+ event.preventDefault();
+ onClose();
});
useEffect(() => {
if (!opened) {
@@ -3174,7 +3197,7 @@ export const AssistantPanel = ({
/>
)}
{opened && (
-