import { ArrowLeft, Bot, ExternalLink, History, Maximize2, Minus, Plus, X } from "lucide-react";
import { useRef } from "react";
import { Link, useLocation } from "react-router";
import { toast } from "sonner";
import { agentMockEnabled, isRunActive, useAgentRun, useCreateAgentRun } from "~/api/agent.ts";
import { StreamingCursor } from "~/components/agentui/streaming-response.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
import { errorMessage } from "~/lib/api.ts";
import { cn } from "~/lib/utils.ts";
import { capabilityMeta, isDenied, RunStatusBadge } from "./agent-conversation.tsx";
import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx";
import { AgentSessionList } from "./agent-session-list.tsx";
import {
type AgentContext,
type AgentDrawerTab,
agentShortcutLabel,
contextFromPath,
contextPrefix,
lastAgentDrawerTab,
minimizeAgentDrawer,
openAgentDrawer,
openAgentHistoryRun,
restoreAgentDrawer,
setAgentDrawerOpen,
setAgentDrawerRun,
setAgentDrawerTab,
showAgentRunInDrawer,
splitContextGoal,
useAgentDrawer,
} from "./agent-store.ts";
import { AgentRunOpenContext } from "./agent-subagent-tree.tsx";
import { quickGoalsFor } from "./quick-goals.ts";
import { useAgentStream } from "./use-agent-stream.ts";
/** 悬浮按钮等"抽屉外但属于抽屉"的控件打这个属性:点它不算点外面(否则先被关掉又被重新打开) */
export const AGENT_LAUNCHER_ATTR = "data-agent-launcher";
const CONTEXT_KEYS: [keyof AgentContext, string][] = [
["entityId", "作品"],
["releaseId", "资源"],
["downloadTaskId", "下载任务"],
["recordId", "整理记录"],
["siteId", "站点"],
["taskId", "任务"],
];
function ContextChips({ context }: { context: AgentContext }) {
const chips = CONTEXT_KEYS.filter(([k]) => context[k]);
if (!chips.length) return null;
return (
{chips.map(([k, label]) => (
-
{label} · {String(context[k]).slice(0, 12)}
))}
);
}
const TABS: { value: AgentDrawerTab; label: string }[] = [
{ value: "current", label: "当前" },
{ value: "history", label: "历史" },
];
function DrawerTabs({ tab }: { tab: AgentDrawerTab }) {
return (
{TABS.map((t) => (
))}
);
}
/** 历史 Tab 里打开的一条会话:返回列表 + 目标 + 在页面打开(次要);正文复用会话组件,进行中可继续输入 */
function HistoryRunView({ runId }: { runId: string }) {
const run = useAgentRun(runId);
const mock = agentMockEnabled();
return (
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
);
}
/**
* Agent 侧栏抽屉:任意页面可打开,带当前页面上下文(entityId / siteId / taskId 等)作为 goal 前缀;
* 顶部 Tab「当前 / 历史」:历史里点一条在抽屉内打开(不跳页),返回列表保留筛选与滚动位置;抽屉记住上次 Tab。
* 运行中可最小化到右下角小窗,流式输出继续。
* 挂载:在 console layout 里渲染一次 ;顶栏放 。
* 页面内打开:import { openAgentDrawer } from "~/components/agent/agent-store.ts"。
*/
export function AgentDrawer() {
const { open, minimized, context, runId, tab, historyRunId } = useAgentDrawer();
const create = useCreateAgentRun();
const prefix = contextPrefix(context);
const { pathname } = useLocation();
const content = useRef(null);
const history = tab === "history";
return (
<>
{
const ta = content.current?.querySelector("textarea:not([disabled])");
if (!ta) return;
e.preventDefault();
ta.focus();
ta.setSelectionRange(ta.value.length, ta.value.length);
}}
onInteractOutside={(e) => {
if ((e.target as Element | null)?.closest?.(`[${AGENT_LAUNCHER_ATTR}]`)) e.preventDefault();
}}
>
Agent
{context?.label ? `当前对象:${context.label}` : "交代目标,Agent 在授权范围内执行并汇报。"}
({agentShortcutLabel()} 开关,Esc 关闭)
{context && !history ? : null}
{runId && !history ? (
) : null}
{history ? (
historyRunId ? (
) : (
)
) : runId ? (
) : (
create.mutate(
{ goal: `${prefix}${goal}`, ...opts },
{
onSuccess: (run) => setAgentDrawerRun(run.id),
onError: (e) => toast.error(errorMessage(e)),
},
)
}
/>
)}
{history ? null : (
{runId ? (
<>
>
) : (
)}
)}
{minimized && runId ? : null}
>
);
}
/** 最小化后的小窗:保持事件流订阅,显示状态与最新一句输出 */
function AgentMiniDock({ runId }: { runId: string }) {
const run = useAgentRun(runId);
const active = isRunActive(run.data?.run.status);
const live = useAgentStream(runId, active || !run.data);
const last = live.items[live.items.length - 1];
const streaming = active && !live.finished;
const line =
last?.kind === "text"
? last.text.trim().split("\n").pop()
: last?.kind === "tool"
? `${isDenied(last.call) ? "被拒绝" : last.call.status === "running" ? "调用" : "完成"} · ${capabilityMeta(last.call.capabilityId)?.title ?? last.call.capabilityId}`
: last?.kind === "thinking"
? "思考中…"
: last?.kind === "end"
? last.ok
? "已完成,展开查看核实结果"
: last.message
: "等待输出…";
const status = run.data
? live.finished && isRunActive(run.data.run.status)
? "running"
: run.data.run.status
: null;
return (
{status ? : null}
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
);
}
/** 顶栏按钮:有最小化的运行时恢复它;否则按当前路由带上下文打开(页面内按钮用 openAgentDrawer 传更完整的上下文) */
export function AgentDrawerTrigger({ className }: { className?: string }) {
const { minimized, runId } = useAgentDrawer();
const { pathname } = useLocation();
return (
);
}