feat(agent): implement global Agent panel with site navigation and interrupt functionality

This commit is contained in:
HouYunFei
2026-07-09 16:19:32 +08:00
parent 387d8be961
commit 7b347729ec
17 changed files with 319 additions and 1588 deletions
@@ -1,12 +1,13 @@
import { create } from "zustand";
import type { CanvasAgentOp } from "@/lib/canvas/canvas-agent-ops";
import type { CanvasAgentOp, CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
export type AgentChatRole = "user" | "assistant" | "system" | "tool" | "error";
export type AgentAttachment = { id: string; name: string; type: string; size: number; url: string; dataUrl: string };
export type AgentChatItem = { id: string; role: AgentChatRole; title?: string; text: string; meta?: string; detail?: unknown; attachments?: AgentAttachment[]; streamId?: string };
export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown };
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[] } };
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } };
export type AgentCanvasContext = { snapshot: CanvasAgentSnapshot; applyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; undoOps: () => CanvasAgentSnapshot | null; canUndo: boolean };
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
@@ -14,8 +15,12 @@ const CONNECT_TIMEOUT_MS = 6000;
let agentSource: EventSource | null = null;
let connectTimer: ReturnType<typeof setTimeout> | null = null;
type CanvasAgentStore = {
type AgentStore = {
width: number;
panelOpen: boolean;
panelMounted: boolean;
panelClosing: boolean;
canvasContext: AgentCanvasContext | null;
url: string;
token: string;
connected: boolean;
@@ -35,16 +40,26 @@ type CanvasAgentStore = {
activity: string;
connectError: string;
pendingTool: AgentPendingToolCall | null;
setAgentState: (patch: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
setAgentState: (patch: Partial<Omit<AgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs" | "openPanel" | "closePanel" | "togglePanel" | "setCanvasContext">>) => void;
openPanel: () => void;
closePanel: () => void;
togglePanel: () => void;
setCanvasContext: (context: AgentCanvasContext | null) => void;
connectAgent: () => void;
disconnectAgent: (patch?: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
disconnectAgent: (patch?: Partial<Omit<AgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs" | "openPanel" | "closePanel" | "togglePanel" | "setCanvasContext">>) => void;
addMessage: (item: AgentChatItem) => void;
addEventLog: (item: AgentEventLog) => void;
clearEventLogs: () => void;
};
export const useCanvasAgentStore = create<CanvasAgentStore>((set, get) => ({
export const CANVAS_AGENT_PANEL_MOTION_MS = 500;
export const useAgentStore = create<AgentStore>((set, get) => ({
width: typeof window === "undefined" ? 440 : Number(localStorage.getItem("canvas-agent-panel-width")) || 440,
panelOpen: false,
panelMounted: true,
panelClosing: false,
canvasContext: null,
url: typeof window === "undefined" ? "http://127.0.0.1:17371" : localStorage.getItem("canvas-agent-url") || "http://127.0.0.1:17371",
token: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-token") || "",
connected: false,
@@ -65,6 +80,16 @@ export const useCanvasAgentStore = create<CanvasAgentStore>((set, get) => ({
connectError: "",
pendingTool: null,
setAgentState: (patch) => set(patch),
openPanel: () => set({ panelOpen: true, panelMounted: true, panelClosing: false }),
closePanel: () => {
if (!get().panelMounted || get().panelClosing) return;
set({ panelOpen: false, panelClosing: true });
setTimeout(() => {
if (get().panelClosing) set({ panelMounted: false, panelClosing: false });
}, CANVAS_AGENT_PANEL_MOTION_MS);
},
togglePanel: () => (get().panelOpen ? get().closePanel() : get().openPanel()),
setCanvasContext: (canvasContext) => set({ canvasContext }),
connectAgent: () => {
const endpoint = get().url.trim().replace(/\/$/, "");
const token = get().token.trim();
@@ -75,24 +100,10 @@ export const useCanvasAgentStore = create<CanvasAgentStore>((set, get) => ({
} catch {
return set({ connectError: "Local URL 格式不正确" });
}
get().disconnectAgent({ url: endpoint, token, enabled: true, activity: "连接中", connectError: "" });
localStorage.setItem("canvas-agent-url", endpoint);
localStorage.setItem("canvas-agent-token", token);
const clientId = typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID();
const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}&role=status`);
agentSource = source;
connectTimer = setTimeout(() => {
if (agentSource !== source) return;
get().disconnectAgent({ activity: "连接超时", connectError: "连接超时:请确认 Canvas Agent 正在运行,并使用当前启动输出的 Connect token" });
}, CONNECT_TIMEOUT_MS);
source.addEventListener("hello", () => {
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: true, connected: true, activity: "已连接", connectError: "" });
});
source.onerror = () => {
if (agentSource === source) get().disconnectAgent({ activity: "连接失败", connectError: "连接失败:请检查 Local URL、Connect token 或已绑定的网页 Origin" });
};
// 只设 enabled=true,由 CanvasLocalAgentPanel 的 useEffect 统一负责开 SSE
set({ url: endpoint, token, enabled: true, activity: "连接中", connectError: "" });
},
disconnectAgent: (patch = {}) => {
agentSource?.close();