feat: add AgentRealtimeProvider and integrate it into WorkbenchLayout and ConversationsPage

This commit is contained in:
mlogclub
2026-06-13 12:53:39 +08:00
parent 878d4bb811
commit 7ed6b628c1
8 changed files with 80 additions and 7 deletions
@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { describe, it } from "node:test";
const providerSource = await readFile(
new URL("./agent-realtime-provider.tsx", import.meta.url),
"utf8",
).catch(() => "");
const workbenchLayoutSource = await readFile(
new URL("../app/workbench/layout.tsx", import.meta.url),
"utf8",
);
const dashboardConversationsPageSource = await readFile(
new URL("../app/dashboard/conversations/page.tsx", import.meta.url),
"utf8",
);
const conversationWorkbenchSource = await readFile(
new URL("../app/dashboard/conversations/_components/conversation-workbench.tsx", import.meta.url),
"utf8",
);
describe("AgentRealtimeProvider placement", () => {
it("owns the agent realtime hook", () => {
assert.match(providerSource, /export function AgentRealtimeProvider/);
assert.match(providerSource, /useAgentConversationRealtime\(\)/);
});
it("is mounted at the workbench layout level", () => {
assert.match(workbenchLayoutSource, /import \{ AgentRealtimeProvider \}/);
assert.match(workbenchLayoutSource, /<AgentRealtimeProvider \/>/);
});
it("keeps dashboard conversations realtime without tying it to ConversationWorkbench", () => {
assert.match(dashboardConversationsPageSource, /<AgentRealtimeProvider \/>/);
assert.doesNotMatch(conversationWorkbenchSource, /useAgentConversationRealtime/);
});
});
@@ -0,0 +1,9 @@
"use client"
import { useAgentConversationRealtime } from "@/hooks/use-agent-conversation-realtime"
export function AgentRealtimeProvider() {
useAgentConversationRealtime()
return null
}
@@ -13,4 +13,9 @@ describe("workspace switcher config", () => {
it("wraps the dropdown label in a Base UI menu group", async () => {
assert.match(source, /<DropdownMenuGroup>[\s\S]*<DropdownMenuLabel>\{t\("workspace\.switchWorkspace"\)\}<\/DropdownMenuLabel>/);
});
it("does not auto-open the rail menu from focus events", async () => {
assert.doesNotMatch(source, /onFocus=\{openHoverMenu\}/);
assert.doesNotMatch(source, /onBlur=\{closeHoverMenu\}/);
});
});
+9 -3
View File
@@ -2,7 +2,7 @@
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import Link from "next/link"
import { useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import type { ReactElement } from "react"
import { useI18n } from "@/i18n/provider"
@@ -82,6 +82,14 @@ export function WorkspaceSwitcher({
}, 120)
}
useEffect(() => {
return () => {
if (closeTimerRef.current) {
clearTimeout(closeTimerRef.current)
}
}
}, [])
const triggerClassName = cn(
"gap-2 text-left",
variant === "header" &&
@@ -133,8 +141,6 @@ export function WorkspaceSwitcher({
<DropdownMenuTrigger
onPointerEnter={openHoverMenu}
onPointerLeave={closeHoverMenu}
onFocus={openHoverMenu}
onBlur={closeHoverMenu}
render={
trigger ?? <Button variant="ghost" className={triggerClassName} />
}