feat(plugin): add Codex App plugin support and local agent integration

This commit is contained in:
HouYunFei
2026-06-23 17:35:43 +08:00
parent 8cbe00e348
commit bd0ad0aebf
18 changed files with 518 additions and 28 deletions
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { BookOpen, Bot, Home, ImageIcon, Images, List, Menu, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react";
import { saveAs } from "file-saver";
@@ -42,6 +42,8 @@ import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "../compone
import { CanvasToolbar } from "../components/canvas-toolbar";
import { AssetPickerModal, type InsertAssetPayload } from "../components/asset-picker-modal";
import { CanvasZoomControls } from "../components/canvas-zoom-controls";
import { CanvasLocalAgentPanel } from "../components/canvas-local-agent-panel";
import { useCanvasAgentStore } from "../stores/use-canvas-agent-store";
import { useCanvasStore } from "../stores/use-canvas-store";
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops";
import { buildCanvasResourceReferences, buildNodeMentionReferences } from "../utils/canvas-resource-references";
@@ -219,7 +221,11 @@ function InfiniteCanvasPage() {
const { message, modal } = App.useApp();
const params = useParams<{ id: string }>();
const router = useRouter();
const searchParams = useSearchParams();
const projectId = params.id;
const localAgentConnected = useCanvasAgentStore((state) => state.connected);
const localAgentActivity = useCanvasAgentStore((state) => state.activity);
const localAgentEnabled = useCanvasAgentStore((state) => state.enabled);
const containerRef = useRef<HTMLDivElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const uploadTargetRef = useRef<{ nodeId?: string; position?: Position } | null>(null);
@@ -302,6 +308,8 @@ function InfiniteCanvasPage() {
const [assistantClosing, setAssistantClosing] = useState(false);
const [agentMode, setAgentMode] = useState<CanvasAgentMode>("online");
const [agentUndoSnapshot, setAgentUndoSnapshot] = useState<CanvasAgentSnapshot | null>(null);
const codexAutoConnect = ["new", "recent", "choose"].includes(searchParams.get("mode") || "");
const codexCompactAgent = codexAutoConnect && searchParams.has("agentUrl");
const [titleEditing, setTitleEditing] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
@@ -424,6 +432,15 @@ function InfiniteCanvasPage() {
void restore();
}, [hydrated, openProject, projectId, router]);
useEffect(() => {
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
if (searchParams.has("agentUrl")) {
setAgentMode("local");
return;
}
openAgent("local");
}, [projectLoaded, searchParams]);
useEffect(() => {
if (!projectLoaded || applyingHistoryRef.current || historyPausedRef.current) return;
const next = createHistoryEntry();
@@ -2494,6 +2511,7 @@ function InfiniteCanvasPage() {
onUndo={undoCanvas}
onRedo={redoCanvas}
agentOpen={assistantOpen}
compactAgentStatus={codexCompactAgent ? { connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity } : undefined}
onToggleAgent={() => (assistantOpen ? closeAgent() : openAgent())}
/>
@@ -2775,6 +2793,7 @@ function InfiniteCanvasPage() {
</Modal>
<AssetPickerModal open={assetPickerOpen} onInsert={handleAssetInsert} onClose={() => setAssetPickerOpen(false)} />
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={Boolean(agentUndoSnapshot)} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
</section>
{assistantMounted ? (
<CanvasAssistantPanel
@@ -2791,6 +2810,7 @@ function InfiniteCanvasPage() {
onPasteImage={pasteAssistantImage}
agentMode={agentMode}
onAgentModeChange={setAgentMode}
autoConnectLocal={codexAutoConnect}
closing={assistantClosing}
onCollapse={closeAgent}
/>
@@ -2817,6 +2837,7 @@ function CanvasTopBar({
onUndo,
onRedo,
agentOpen,
compactAgentStatus,
onToggleAgent,
}: {
title: string;
@@ -2836,6 +2857,7 @@ function CanvasTopBar({
onUndo: () => void;
onRedo: () => void;
agentOpen: boolean;
compactAgentStatus?: { connected: boolean; enabled: boolean; activity: string };
onToggleAgent: () => void;
}) {
const colorTheme = useThemeStore((state) => state.theme);
@@ -2907,6 +2929,7 @@ function CanvasTopBar({
</div>
<div className="pointer-events-auto flex items-center gap-1.5">
{compactAgentStatus ? <CompactAgentStatus status={compactAgentStatus} onClick={onToggleAgent} /> : null}
<UserStatusActions
variant="canvas"
onOpenShortcuts={() => setShortcutsOpen(true)}
@@ -2953,6 +2976,25 @@ function MenuLabel({ text, shortcut }: { text: string; shortcut: string }) {
);
}
function CompactAgentStatus({ status, onClick }: { status: { connected: boolean; enabled: boolean; activity: string }; onClick: () => void }) {
const colorTheme = useThemeStore((state) => state.theme);
const theme = canvasThemes[colorTheme];
const label = status.connected ? "已连接到本地 Codex" : status.enabled ? status.activity || "连接中" : "正在连接本地 Codex";
const dotColor = status.connected ? "#22c55e" : status.enabled ? "#f59e0b" : theme.node.muted;
return (
<button
type="button"
className="flex h-10 items-center gap-2 rounded-xl px-3 text-sm font-medium transition hover:opacity-85"
style={{ background: theme.toolbar.panel, color: theme.node.text, boxShadow: "0 10px 30px rgba(28,25,23,.10)" }}
onClick={onClick}
title="打开本地 Codex 面板"
>
<span className="size-2 rounded-full" style={{ background: dotColor }} />
<span className="max-w-[180px] truncate">{label}</span>
</button>
);
}
function Shortcut({ keys, value }: { keys: string[]; value: string }) {
return (
<div className="grid grid-cols-[minmax(0,1fr)_120px] items-center gap-6 rounded-lg px-1 py-1.5">