fix(web): agent FAB slides away while scrolling down (QA U6)

Page-level scroll areas only; it returns on scroll up, near the top or at the
bottom, and when focused by keyboard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 20:09:29 +08:00
1 parent 312e70d0bc
commit 9b017906cf
1 file changed
+31 -6
@@ -101,6 +101,29 @@ function PulseDot({ className }: { className?: string }) {
</span>
);
}
/**
* QA U6:页面向下滚动时 FAB 滑出右侧,向上滚动、停在页底或页面很短时回来,
* 滚动途中不再盖住右对齐的开关与状态栏。只看页面级滚动区域(高度 ≥ 半屏),列表内小滚动区不算。
*/
function useHideOnScroll(): boolean {
const [hidden, setHidden] = useState(false);
useEffect(() => {
const last = new WeakMap<object, number>();
const onScroll = (e: Event) => {
const t = e.target === document ? document.scrollingElement : e.target;
if (!(t instanceof HTMLElement) || t.clientHeight < window.innerHeight / 2) return;
const top = t.scrollTop;
const prev = last.get(t) ?? top;
last.set(t, top);
const atBottom = top + t.clientHeight >= t.scrollHeight - 4;
if (atBottom || top < 80 || top - prev < -8) setHidden(false);
else if (top - prev > 8) setHidden(true);
};
document.addEventListener("scroll", onScroll, { capture: true, passive: true });
return () => document.removeEventListener("scroll", onScroll, { capture: true });
}, []);
return hidden;
}
/**
* 右下角悬浮按钮 + 全局快捷键。挂在 console layout 里一次。
@@ -113,6 +136,7 @@ export function AgentLauncher() {
const { open, minimized, runId } = useAgentDrawer();
const { count, summary } = useActiveAgentRuns();
const lift = useFloatingBarLift();
const scrolledAway = useHideOnScroll();
const shortcut = agentShortcutLabel();
// 挂到 body:外壳是 isolate 的层叠上下文,留在里面的话抽屉打开后"收起"会被遮罩盖住
const [host, setHost] = useState<HTMLElement | null>(null);
@@ -121,7 +145,7 @@ export function AgentLauncher() {
const onAgentPage = /^\/console\/agent(\/|$)/.test(pathname);
if (!host || (onAgentPage && !open) || (minimized && runId && !open)) return null;
const label = open ? "收起 Agent" : count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent";
const label = open ? "收起 Agent" : count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent";
return createPortal(
<button
type="button"
@@ -138,6 +162,7 @@ export function AgentLauncher() {
"animate-in fade-in-0 zoom-in-95 duration-base motion-reduce:animate-none",
"md:px-2 md:pr-5",
open && "z-[60] hidden md:right-[calc(36rem+1rem)] md:flex",
!open && scrolledAway && "translate-x-[calc(100%+2rem)] focus-visible:translate-x-0",
)}
>
<span className="relative grid size-10 shrink-0 place-items-center rounded-full pill-selected">
@@ -154,7 +179,7 @@ export function AgentLauncher() {
</kbd>
</span>
<span className="max-w-56 truncate text-meta leading-4 text-muted-foreground">
{open ? "Esc 关闭" : (summary ?? "交代目标,边做边汇报")}
{open ? "Esc 关闭" : (summary ?? "交代目标,边做边汇报")}
</span>
</span>
</button>,
@@ -180,7 +205,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
{...{ [AGENT_LAUNCHER_ATTR]: "" }}
onClick={activate}
title={title}
aria-label={count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"}
aria-label={count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"}
aria-keyshortcuts="Control+J Meta+J"
className="prism-ring relative mb-2 grid h-10 w-10 shrink-0 cursor-pointer place-items-center rounded-xl text-foreground outline-none transition-transform duration-micro active:scale-[0.95] focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -207,7 +232,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
{count ? <PulseDot /> : null}
</span>
<span className="truncate text-meta text-muted-foreground">
{count ? `${count} 个会话进行中` : "交代目标,边做边汇报"}
{count ? `${count} 个会话进行中` : "交代目标,边做边汇报"}
</span>
</span>
{count ? (
@@ -225,9 +250,9 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
export function AgentShortcutsHelp() {
const shortcut = agentShortcutLabel();
const rows: [string, string][] = [
[shortcut, "打开 / 收起 Agent,附带当前页面"],
[shortcut, "打开 / 收起 Agent,附带当前页面"],
["Esc", "关闭 Agent"],
["Enter", "发送目标;Shift+Enter 换行"],
["Enter", "发送目标;Shift+Enter 换行"],
];
return (
<section aria-labelledby="agent-shortcuts-title" className="space-y-2">