import { Bot, ChevronsRight, Keyboard } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useLocation } from "react-router"; import { isRunActive, RUN_STATUS_LABEL, useAgentRuns } from "~/api/agent.ts"; import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx"; import { agentShortcutLabel, isAgentShortcut, splitContextGoal, toggleAgentDrawer, useAgentDrawer, } from "~/components/agent/agent-store.ts"; import { cn } from "~/lib/utils.ts"; // Agent 的三处醒目入口:右下角悬浮按钮(FAB)、侧栏最上方主入口、全局快捷键 Ctrl/⌘+J。 // 三者都走 toggleAgentDrawer,行为一致:打开中 → 收起;有最小化的运行 → 恢复;否则带当前页面上下文打开。 /** 进行中的会话数与最近一条的状态摘要(列表里有进行中的会话时 5 秒轮询一次) */ export function useActiveAgentRuns() { const runs = useAgentRuns(); return useMemo(() => { const active = (runs.data?.pages ?? []).flatMap((p) => p.items).filter((r) => isRunActive(r.status)); const latest = active[0]; return { count: active.length, summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${splitContextGoal(latest.goal).text}` : null, }; }, [runs.data]); } /** 全局快捷键:Ctrl/⌘+J 切换抽屉(Esc 关闭由抽屉自身处理) */ function useAgentHotkey() { const { pathname } = useLocation(); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.repeat || !isAgentShortcut(e)) return; e.preventDefault(); toggleAgentDrawer(pathname); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [pathname]); } /** 页面里的底部浮动操作栏(文件页批量操作等):FAB 与它横向重叠时抬到它上面 */ const FLOATING_BARS = '[data-floating-bar], [role="toolbar"][aria-label="批量操作"]'; const BASE_BOTTOM = 16; function useFloatingBarLift(): number { const [lift, setLift] = useState(0); useEffect(() => { let frame = 0; const measure = () => { frame = 0; let next = 0; for (const bar of document.querySelectorAll(FLOATING_BARS)) { const r = bar.getBoundingClientRect(); // 只看贴底、且伸到屏幕右侧 FAB 所在区域(右 30%)的栏 if (r.height === 0 || r.bottom < window.innerHeight - 160 || r.right < window.innerWidth * 0.7) continue; next = Math.max(next, window.innerHeight - r.top + 12 - BASE_BOTTOM); } setLift((prev) => (prev === next ? prev : next)); }; const schedule = () => { if (!frame) frame = requestAnimationFrame(measure); }; const mo = new MutationObserver(schedule); mo.observe(document.body, { childList: true, subtree: true }); window.addEventListener("resize", schedule); measure(); return () => { mo.disconnect(); window.removeEventListener("resize", schedule); if (frame) cancelAnimationFrame(frame); }; }, []); return lift; } function CountBadge({ count, className }: { count: number; className?: string }) { if (!count) return null; return ( {count > 99 ? "99+" : count} ); } /** 进行中的脉冲点:减少动态效果时只留静态点 */ function PulseDot({ className }: { className?: string }) { return ( ); } /** * 右下角悬浮按钮 + 全局快捷键。挂在 console layout 里一次。 * - Agent 会话页本身不显示(页面就是 Agent);最小化小窗在右下角时让位给小窗。 * - 抽屉打开时变成"收起",停在抽屉左侧(窄屏抽屉占满全宽,用抽屉自带的关闭钮,FAB 隐藏)。 */ export function AgentLauncher() { useAgentHotkey(); const { pathname } = useLocation(); const { open, minimized, runId } = useAgentDrawer(); const { count, summary } = useActiveAgentRuns(); const lift = useFloatingBarLift(); const shortcut = agentShortcutLabel(); // 挂到 body:外壳是 isolate 的层叠上下文,留在里面的话抽屉打开后"收起"会被遮罩盖住 const [host, setHost] = useState(null); useEffect(() => setHost(document.body), []); const onAgentPage = /^\/console\/agent(\/|$)/.test(pathname); if (!host || (onAgentPage && !open) || (minimized && runId && !open)) return null; const label = open ? "收起 Agent" : count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"; return createPortal( , host, ); } /** 侧栏最上方的 Agent 主入口:强调样式(棱镜环),显示进行中会话数 */ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => void }) { const { pathname } = useLocation(); const { count, summary } = useActiveAgentRuns(); const shortcut = agentShortcutLabel(); const activate = () => { onNavigate?.(); toggleAgentDrawer(pathname); }; const title = `Agent(${shortcut})${summary ? `\n${summary}` : ""}`; if (collapsed) { return ( ); } return ( ); } /** 快捷键说明(设置页"快捷键"一节用) */ export function AgentShortcutsHelp() { const shortcut = agentShortcutLabel(); const rows: [string, string][] = [ [shortcut, "打开 / 收起 Agent,附带当前页面"], ["Esc", "关闭 Agent"], ["Enter", "发送目标;Shift+Enter 换行"], ]; return (

快捷键

{rows.map(([k, v]) => (
{k}
{v}
))}
); }