feat(plugin): add Codex App plugin support and local agent integration
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user