+ {/* drag bar / header */}
+
+
+
{t("AI chat")}
+
+
+ {totalTokens > 0 && (
+
+ {formatTokens(totalTokens)}
+
+ )}
+
+
+
+
+
+
+ setWindowOpen(false)}
+ >
+
+
+
+
+
+ {/* Body is ALWAYS rendered (just hidden via .minimized .content CSS when
+ minimized) so ChatThread — and its useChat store/AbortController —
+ stays mounted and an in-flight stream is never aborted. */}
+
+ {/* history */}
+
+
+
setHistoryOpen((o) => !o)}
+ >
+
+ {t("Chat history")}
+
+
+
+ {t("New chat")}
+
+
+ {historyOpen && (
+
+
+
+ )}
+
+
+ {/* body: active chat thread */}
+
+ {waitingForHistory ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {/* resize affordance icon (drawn manually; native resizer is hidden) */}
+ {!minimized && (
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/client/src/features/ai-chat/types/ai-chat.types.ts b/apps/client/src/features/ai-chat/types/ai-chat.types.ts
index fd5e59f8..f65890ec 100644
--- a/apps/client/src/features/ai-chat/types/ai-chat.types.ts
+++ b/apps/client/src/features/ai-chat/types/ai-chat.types.ts
@@ -26,7 +26,16 @@ export interface IAiChatMessageRow {
role: "user" | "assistant" | string;
content: string | null;
toolCalls?: unknown;
- metadata?: { parts?: UIMessage["parts"] } | null;
+ metadata?: {
+ parts?: UIMessage["parts"];
+ // AI SDK v6 `totalUsage` persisted on assistant rows. Used to sum the token
+ // count shown in the floating window's header badge.
+ usage?: {
+ inputTokens?: number;
+ outputTokens?: number;
+ totalTokens?: number;
+ };
+ } | null;
createdAt: string;
}
diff --git a/apps/client/src/features/page-history/components/history-item.tsx b/apps/client/src/features/page-history/components/history-item.tsx
index a8f88a89..e2f02110 100644
--- a/apps/client/src/features/page-history/components/history-item.tsx
+++ b/apps/client/src/features/page-history/components/history-item.tsx
@@ -8,8 +8,10 @@ import { IPageHistory } from "@/features/page-history/types/page.types";
import { memo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useSetAtom } from "jotai";
-import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
-import { activeAiChatIdAtom } from "@/features/ai-chat/atoms/ai-chat-atom.ts";
+import {
+ activeAiChatIdAtom,
+ aiChatWindowOpenAtom,
+} from "@/features/ai-chat/atoms/ai-chat-atom.ts";
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
const MAX_VISIBLE_AVATARS = 5;
@@ -27,9 +29,9 @@ interface HistoryItemProps {
* Badge marking a version written by the AI agent (provenance C3 / §7.4). It is
* ADDITIVE — shown next to the human author, never replacing them. When the
* version carries an `aiChatId`, clicking the badge deep-links into that chat:
- * it sets the active-chat atom, opens the AI-chat aside tab, and closes the
- * history modal. The click is contained (stopPropagation) so it does not also
- * trigger the row's version-select.
+ * it sets the active-chat atom, opens the floating AI-chat window, and closes
+ * the history modal. The click is contained (stopPropagation) so it does not
+ * also trigger the row's version-select.
*/
function AiAgentBadge({
authorName,
@@ -39,7 +41,7 @@ function AiAgentBadge({
aiChatId?: string | null;
}) {
const { t } = useTranslation();
- const setAsideState = useSetAtom(asideStateAtom);
+ const setAiChatWindowOpen = useSetAtom(aiChatWindowOpenAtom);
const setActiveChatId = useSetAtom(activeAiChatIdAtom);
const setHistoryModalOpen = useSetAtom(historyAtoms);
@@ -52,10 +54,10 @@ function AiAgentBadge({
event.stopPropagation();
if (!aiChatId) return;
setActiveChatId(aiChatId);
- setAsideState({ tab: "ai-chat", isAsideOpen: true });
+ setAiChatWindowOpen(true);
setHistoryModalOpen(false);
},
- [aiChatId, setActiveChatId, setAsideState, setHistoryModalOpen],
+ [aiChatId, setActiveChatId, setAiChatWindowOpen, setHistoryModalOpen],
);
const badge = (
diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx
index fd0d850c..c802bc44 100644
--- a/apps/client/src/features/page/components/header/page-header-menu.tsx
+++ b/apps/client/src/features/page/components/header/page-header-menu.tsx
@@ -20,7 +20,8 @@ import {
} from "@tabler/icons-react";
import React, { useEffect, useRef, useState } from "react";
import { useAsideTriggerProps } from "@/hooks/use-toggle-aside.tsx";
-import { useAtom, useAtomValue } from "jotai";
+import { useAtom, useAtomValue, useSetAtom } from "jotai";
+import { aiChatWindowOpenAtom } from "@/features/ai-chat/atoms/ai-chat-atom.ts";
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
import { useDisclosure, useHotkeys } from "@mantine/hooks";
import { useClipboard } from "@/hooks/use-clipboard";
@@ -64,7 +65,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
const { t } = useTranslation();
const commentsTriggerProps = useAsideTriggerProps("comments");
const tocTriggerProps = useAsideTriggerProps("toc");
- const aiChatTriggerProps = useAsideTriggerProps("ai-chat");
+ const setAiChatWindowOpen = useSetAtom(aiChatWindowOpenAtom);
const { pageSlug } = useParams();
const { data: page } = usePageQuery({
pageId: extractPageSlugId(pageSlug),
@@ -137,7 +138,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
variant="subtle"
color="dark"
aria-label={t("AI chat")}
- {...aiChatTriggerProps}
+ onClick={() => setAiChatWindowOpen((v) => !v)}
>