diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 94ec3e1c..9e932695 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@uiw/react-codemirror": "^4.25.11", "@xyflow/react": "^12.11.0", @@ -753,6 +754,21 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@dagrejs/dagre": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@dagrejs/dagre/-/dagre-3.0.0.tgz", + "integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "4.0.1" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@dagrejs/graphlib/-/graphlib-4.0.1.tgz", + "integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 06042f48..9ad31b4e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@uiw/react-codemirror": "^4.25.11", "@xyflow/react": "^12.11.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8d9e0471..45dc51ee 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import { createSession, DEFAULT_STUDIO_ACCESS, DEFAULT_SITE_BRANDING, + deleteRuntime, deleteMedia, deleteSessionMedia, deleteSession, @@ -31,6 +32,7 @@ import { getSessionCapabilities, getSession, getStudioAccess, + getRuntimes, listApps, listSessionBuiltinTools, listSessions, @@ -47,7 +49,7 @@ import { type AddSessionCapability, type Attachment, type FrontendInvocation, - type ManagedRuntime, + type CloudRuntime, type MessageFeedbackRating, type SiteBranding, type SessionCapabilities, @@ -67,14 +69,19 @@ import { AgentInfoDrawer, AgentInfoPanel } from "./ui/AgentTopology"; import { AgentIdentityIcon } from "./ui/AgentIdentityIcon"; import { SkillCenterView } from "./ui/SkillCenter"; import { AddAgentKitView } from "./ui/AddAgentKit"; -import { ManageAgentsView } from "./ui/ManageAgents"; +import { + AgentWorkspace, + type WorkspaceAgentDraft, +} from "./ui/AgentWorkspace"; import { SearchView } from "./ui/Search"; import { buildAgentEntries, connectRuntime, loadConnections, registerConnections, + removeRuntimeConnection, remoteAppId, + type AgentEntry, type RemoteConnection, } from "./adk/connections"; import { Blocks, ThinkingPlaceholder } from "./ui/Blocks"; @@ -90,7 +97,7 @@ import { WorkflowCreate } from "./create/WorkflowCreate"; import { CodePackageCreate } from "./create/CodePackageCreate"; import { FileArchive } from "lucide-react"; import type { AgentDraft } from "./create/types"; -import type { DeploymentTaskUpdate } from "./ui/ProjectPreview"; +import type { DeployResult, DeploymentTaskUpdate } from "./ui/ProjectPreview"; import { DeploymentErrorMessage } from "./ui/DeploymentErrorMessage"; import { TextShimmer } from "./ui/text-shimmer/TextShimmer"; import { StudioUpdateControl } from "./ui/StudioUpdateControl"; @@ -142,6 +149,40 @@ function emptyInvocation(): FrontendInvocation { return { skills: [] }; } +function workspaceDraftsKey(userId: string): string { + return `veadk.agentDrafts.${encodeURIComponent(userId)}`; +} + +function activeWorkspaceDraftKey(userId: string): string { + return `${workspaceDraftsKey(userId)}.active`; +} + +function workspaceAgentOrderKey(userId: string): string { + return `veadk.agentOrder.${encodeURIComponent(userId)}`; +} + +function loadWorkspaceDrafts(userId: string): WorkspaceAgentDraft[] { + if (!userId) return []; + try { + const value = JSON.parse(localStorage.getItem(workspaceDraftsKey(userId)) || "[]"); + return Array.isArray(value) ? value : []; + } catch { + return []; + } +} + +function loadWorkspaceAgentOrder(userId: string): string[] { + if (!userId) return []; + try { + const value = JSON.parse(localStorage.getItem(workspaceAgentOrderKey(userId)) || "[]"); + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === "string") + : []; + } catch { + return []; + } +} + function findAgentNode(node: AgentNode, name: string): AgentNode | undefined { if (node.name === name || node.id === name) return node; for (const child of node.children) { @@ -896,8 +937,12 @@ export default function App() { const [addMenu, setAddMenu] = useState(false); // A draft imported from YAML, used to pre-fill the custom wizard once. const [importedDraft, setImportedDraft] = useState(null); + const [savedAgentDrafts, setSavedAgentDrafts] = useState([]); + const [workspaceAgentOrder, setWorkspaceAgentOrder] = useState([]); + const [editingDraftId, setEditingDraftId] = useState(""); + const editingDraftBaselineRef = useRef(null); const [searchView, setSearchView] = useState(false); - // The "管理 Agent" view: lists/deletes the current user's AgentKit runtimes. + // The #748 Agent workspace: library, evaluation groups, and draft management. const [manageAgents, setManageAgents] = useState(false); // A search result may belong to a different agent; remember it so the // agent-switch effect opens it instead of resetting to a fresh chat. @@ -909,6 +954,22 @@ export default function App() { registerConnections(c); return c; }); + const [agentLibraryLoading, setAgentLibraryLoading] = useState(false); + const [agentLibraryError, setAgentLibraryError] = useState(""); + const [libraryRuntimeIds, setLibraryRuntimeIds] = useState | null>( + null, + ); + const [libraryRuntimePermissions, setLibraryRuntimePermissions] = useState< + Record + >({}); + const [runtimeUpdateTarget, setRuntimeUpdateTarget] = useState<{ + runtimeId: string; + name: string; + region: string; + currentVersion?: number | null; + } | null>(null); + const [focusedDeploymentTaskId, setFocusedDeploymentTaskId] = useState(""); + const [focusedWorkspaceAgentId, setFocusedWorkspaceAgentId] = useState(""); // Shown when the user clicks the breadcrumb root to leave a create mode; // warns that the in-progress draft will be discarded. const [confirmLeave, setConfirmLeave] = useState(false); @@ -916,6 +977,224 @@ export default function App() { const restoredRef = useRef(false); const defaultViewAppliedRef = useRef(false); + const saveWorkspaceDraft = useCallback( + ( + id: string, + draft: AgentDraft, + deploymentTarget?: WorkspaceAgentDraft["deploymentTarget"], + ) => { + if (!id || !userId) return; + setSavedAgentDrafts((current) => { + const nextItem: WorkspaceAgentDraft = { + id, + draft, + updatedAt: Date.now(), + deploymentTarget, + }; + const next = [nextItem, ...current.filter((item) => item.id !== id)]; + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + }, + [userId], + ); + + const removeWorkspaceDraft = useCallback((id: string) => { + if (!id || !userId) return; + setSavedAgentDrafts((current) => { + const next = current.filter((item) => item.id !== id); + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + }, [userId]); + + const deleteWorkspaceDrafts = useCallback((draftsToDelete: WorkspaceAgentDraft[]) => { + if (!userId || draftsToDelete.length === 0) return; + const deletedDraftIds = new Set(draftsToDelete.map((item) => item.id)); + setSavedAgentDrafts((current) => { + const next = current.filter((item) => !deletedDraftIds.has(item.id)); + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + if (deletedDraftIds.has(editingDraftId)) { + setEditingDraftId(""); + setImportedDraft(null); + setRuntimeUpdateTarget(null); + editingDraftBaselineRef.current = null; + localStorage.removeItem(activeWorkspaceDraftKey(userId)); + } + }, [editingDraftId, userId]); + + const restoreWorkspaceDraftBaseline = useCallback((id: string) => { + if (!id || !userId) return; + const baseline = editingDraftBaselineRef.current; + setSavedAgentDrafts((current) => { + const remaining = current.filter((item) => item.id !== id); + const next = baseline?.id === id ? [baseline, ...remaining] : remaining; + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + }, [userId]); + + useEffect(() => { + if (!userId) { + setSavedAgentDrafts([]); + setWorkspaceAgentOrder([]); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + return; + } + const nextDrafts = loadWorkspaceDrafts(userId); + setSavedAgentDrafts(nextDrafts); + setWorkspaceAgentOrder(loadWorkspaceAgentOrder(userId)); + const activeId = localStorage.getItem(activeWorkspaceDraftKey(userId)) || ""; + const activeDraft = nextDrafts.find((item) => item.id === activeId); + editingDraftBaselineRef.current = activeDraft ?? null; + if (createView === "custom" && activeDraft) { + setEditingDraftId(activeDraft.id); + setImportedDraft(activeDraft.draft); + setRuntimeUpdateTarget(activeDraft.deploymentTarget ?? null); + } + // Restore only when identity changes; later edits are already in state. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userId]); + + useEffect(() => { + if (!userId) return; + const key = activeWorkspaceDraftKey(userId); + if (createView === "custom" && editingDraftId) { + localStorage.setItem(key, editingDraftId); + } else { + localStorage.removeItem(key); + } + }, [createView, editingDraftId, userId]); + + const saveWorkspaceAgentOrder = useCallback((nextOrder: string[]) => { + if (!userId) return; + const deduped = [...new Set(nextOrder.filter(Boolean))]; + setWorkspaceAgentOrder(deduped); + localStorage.setItem(workspaceAgentOrderKey(userId), JSON.stringify(deduped)); + }, [userId]); + + const deleteWorkspaceAgents = useCallback(async (agentsToDelete: AgentEntry[]) => { + const targets = agentsToDelete.filter( + (agent): agent is AgentEntry & { runtimeId: string } => + Boolean(agent.runtimeId) && agent.canDelete === true, + ); + if (targets.length === 0) return; + + const deletedRuntimeIds = new Set(); + const deletedAgentIds = new Set(); + const failures: string[] = []; + for (const agent of targets) { + try { + await deleteRuntime(agent.runtimeId, agent.region ?? "cn-beijing"); + removeRuntimeConnection(agent.runtimeId); + deletedRuntimeIds.add(agent.runtimeId); + deletedAgentIds.add(agent.id); + } catch (cause) { + const message = cause instanceof Error ? cause.message : String(cause); + failures.push(`${agent.label}: ${message}`); + } + } + + if (deletedRuntimeIds.size > 0) { + setConnections(loadConnections()); + setLibraryRuntimeIds((current) => { + if (!current) return current; + const next = new Set(current); + for (const runtimeId of deletedRuntimeIds) next.delete(runtimeId); + return next; + }); + setLibraryRuntimePermissions((current) => + Object.fromEntries( + Object.entries(current).filter(([runtimeId]) => !deletedRuntimeIds.has(runtimeId)), + ), + ); + setWorkspaceAgentOrder((current) => { + const next = current.filter((id) => !deletedAgentIds.has(id)); + if (userId) { + localStorage.setItem(workspaceAgentOrderKey(userId), JSON.stringify(next)); + } + return next; + }); + setSavedAgentDrafts((current) => { + const next = current.filter( + (item) => + !item.deploymentTarget?.runtimeId || + !deletedRuntimeIds.has(item.deploymentTarget.runtimeId), + ); + if (userId) { + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + } + return next; + }); + if (targets.some((agent) => agent.id === appName)) { + viewSidRef.current = ""; + setSessionId(""); + setAppName(""); + } + } + + if (failures.length > 0) { + const shown = failures.slice(0, 3).join(";"); + const suffix = failures.length > 3 ? `;另有 ${failures.length - 3} 个失败` : ""; + throw new Error(`${failures.length} 个 Agent 删除失败:${shown}${suffix}`); + } + }, [appName, userId]); + + const refreshAgentLibrary = useCallback(async () => { + setAgentLibraryLoading(true); + setAgentLibraryError(""); + try { + const runtimes: CloudRuntime[] = []; + let nextToken = ""; + do { + const page = await getRuntimes({ + scope: "mine", + region: "all", + pageSize: 100, + nextToken, + }); + runtimes.push(...page.runtimes); + nextToken = page.nextToken; + } while (nextToken && runtimes.length < 2000); + + setLibraryRuntimeIds(new Set(runtimes.map((runtime) => runtime.runtimeId))); + setLibraryRuntimePermissions( + Object.fromEntries( + runtimes.map((runtime) => [ + runtime.runtimeId, + { canDelete: runtime.canDelete }, + ]), + ), + ); + const failures: string[] = []; + for (const runtime of runtimes) { + try { + await connectRuntime( + runtime.runtimeId, + runtime.name, + runtime.region, + runtime.currentVersion, + ); + } catch { + failures.push(runtime.name); + } + } + setConnections(loadConnections()); + if (failures.length > 0) { + setAgentLibraryError( + `${failures.length} 个 Runtime 暂时无法读取,请稍后重试。`, + ); + } + } catch (cause) { + setAgentLibraryError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setAgentLibraryLoading(false); + } + }, []); + // Placeholder: persisting/registering the created agent is a follow-up. function onCreate(draft: AgentDraft) { console.log("create agent draft:", draft); @@ -927,10 +1206,56 @@ export default function App() { function onAgentAdded(agentId: string, agentName: string) { console.log("Agent added, navigating to:", agentId, agentName); setConnections(loadConnections()); // Refresh connections to pick up the new agent + setLibraryRuntimeIds(null); + removeWorkspaceDraft(editingDraftId); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(agentId); setCreateView(null); + setManageAgents(true); setAppName(agentId); // startNewChat will be called automatically by the appName change effect } + + const openDeploymentDetail = useCallback((task: DeploymentTaskUpdate) => { + setCreateView(null); + setAddMenu(false); + setManageAgents(true); + setFocusedWorkspaceAgentId(""); + setFocusedDeploymentTaskId(task.id); + setError(""); + }, []); + + const finishDeployment = useCallback( + async (result: DeployResult) => { + if (!result.runtimeId) throw new Error("部署完成,但未返回 Runtime ID。"); + const fallbackRegion = runtimeUpdateTarget?.region ?? "cn-beijing"; + const agentId = await connectRuntime( + result.runtimeId, + result.agentName, + result.region ?? fallbackRegion, + result.version, + ); + setConnections(loadConnections()); + setLibraryRuntimeIds((current) => { + const next = new Set(current ?? []); + next.add(result.runtimeId!); + return next; + }); + setRuntimeUpdateTarget(null); + removeWorkspaceDraft(editingDraftId); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(agentId); + setCreateView(null); + setManageAgents(true); + setAppName(agentId); + }, + [editingDraftId, removeWorkspaceDraft, runtimeUpdateTarget], + ); const scrollRef = useRef(null); const conversationAutoFollowRef = useRef(true); const conversationSmoothScrollRef = useRef(false); @@ -2043,8 +2368,32 @@ export default function App() { const visibleCreateView = canCreateAgents ? createView : null; const showAddMenu = canCreateAgents && addMenu; const showAddAgent = canCreateAgents && addAgent; - const showManageAgents = canManageAgents && manageAgents; + const showManageAgents = manageAgents; const agentEntries = buildAgentEntries(apps, connections); + const workspaceAgentEntries: AgentEntry[] = agentEntries + .filter( + (entry) => + entry.runtimeId && + (libraryRuntimeIds === null || libraryRuntimeIds.has(entry.runtimeId)), + ) + .map((entry) => ({ + ...entry, + canDelete: entry.runtimeId + ? libraryRuntimePermissions[entry.runtimeId]?.canDelete === true + : false, + })); + const orderedWorkspaceAgentEntries: AgentEntry[] = (() => { + if (workspaceAgentEntries.length === 0) return workspaceAgentEntries; + const orderIndex = new Map(workspaceAgentOrder.map((id, index) => [id, index])); + return [...workspaceAgentEntries].sort((left, right) => { + const leftIndex = orderIndex.get(left.id); + const rightIndex = orderIndex.get(right.id); + if (leftIndex != null && rightIndex != null) return leftIndex - rightIndex; + if (leftIndex != null) return -1; + if (rightIndex != null) return 1; + return workspaceAgentEntries.indexOf(left) - workspaceAgentEntries.indexOf(right); + }); + })(); const labelOf = (id: string) => agentEntries.find((e) => e.id === id)?.label ?? id; // The runtime backing the current selection (if it's a cloud runtime app) — // drives the picker's side detail panel. @@ -2142,14 +2491,6 @@ export default function App() { setSessionId(""); setAppName(id); }; - const connectManagedRuntime = async (runtime: ManagedRuntime) => { - const agentId = await connectRuntime( - runtime.runtimeId, - runtime.name, - runtime.region, - ); - selectAgent(agentId); - }; return (
@@ -2224,10 +2565,6 @@ export default function App() { setError(""); }} onManageAgents={() => { - if (!canManageAgents) { - setError("当前账号没有管理 Agent 的权限。"); - return; - } if (sandboxSession) exitSandboxSession(); viewSidRef.current = ""; setSessionId(""); @@ -2238,6 +2575,7 @@ export default function App() { setSearchView(false); setManageAgents(true); setError(""); + void refreshAgentLibrary(); }} onPickSession={(id) => { setCreateView(null); @@ -2420,7 +2758,7 @@ export default function App() { : showAddAgent ? "添加 AgentKit 智能体" : showManageAgents - ? "管理 Agent" + ? "智能体" : undefined } titleLeading={ @@ -2488,9 +2826,79 @@ export default function App() { )} {showManageAgents ? ( - void refreshAgentLibrary()} + onAgentOrderChange={saveWorkspaceAgentOrder} + onDeleteAgents={deleteWorkspaceAgents} + onDeleteDrafts={deleteWorkspaceDrafts} + onSelectAgent={selectAgent} + onCreateAgent={() => { + if (!canCreateAgents) { + setError("当前账号没有添加 Agent 的权限。"); + return; + } + setManageAgents(false); + setAddMenu(true); + setCreateView(null); + setImportedDraft(null); + setRuntimeUpdateTarget(null); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setError(""); + }} + onUpdateAgent={(nextDraft) => { + if (!canManageAgents && !canCreateAgents) { + setError("当前账号没有管理 Agent 的权限。"); + return; + } + if (!currentConn?.runtimeId) { + setError("仅支持更新已部署的云端智能体。"); + return; + } + setManageAgents(false); + setImportedDraft(nextDraft); + const nextDraftId = `runtime-${currentConn.runtimeId}`; + setEditingDraftId(nextDraftId); + editingDraftBaselineRef.current = + savedAgentDrafts.find((item) => item.id === nextDraftId) ?? null; + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setRuntimeUpdateTarget({ + runtimeId: currentConn.runtimeId, + name: currentConn.name, + region: currentConn.region ?? "cn-beijing", + currentVersion: currentConn.currentVersion, + }); + setCreateView("custom"); + setError(""); + }} + onEditDraft={(item) => { + setManageAgents(false); + setImportedDraft(item.draft); + setEditingDraftId(item.id); + editingDraftBaselineRef.current = item; + setRuntimeUpdateTarget(item.deploymentTarget ?? null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setCreateView("custom"); + setError(""); + }} /> ) : showAddMenu ? ( { setImportedDraft(null); + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setEditingDraftId( + k === "custom" ? `draft-${Date.now().toString(36)}` : "", + ); + editingDraftBaselineRef.current = null; setCreateView(k); }} onImport={(d) => { setImportedDraft(d); + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setEditingDraftId(`draft-${Date.now().toString(36)}`); + editingDraftBaselineRef.current = null; setCreateView("custom"); }} /> @@ -2586,12 +3006,41 @@ export default function App() { /> ) : visibleCreateView === "custom" ? ( setCreateView("menu")} onCreate={onCreate} onAgentAdded={onAgentAdded} features={features} onDeploymentTaskChange={updateDeploymentTask} + deploymentTarget={runtimeUpdateTarget ?? undefined} + onDraftChange={(draft, dirty) => { + if (!editingDraftId) return; + if (dirty) { + saveWorkspaceDraft( + editingDraftId, + draft, + runtimeUpdateTarget ?? undefined, + ); + } else { + restoreWorkspaceDraftBaseline(editingDraftId); + } + }} + onDiscard={editingDraftId ? () => { + restoreWorkspaceDraftBaseline(editingDraftId); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + setImportedDraft(null); + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(appName); + setCreateView(null); + setAddMenu(false); + setManageAgents(true); + setError(""); + } : undefined} + onDeploymentStarted={openDeploymentDetail} + onDeploymentComplete={finishDeployment} /> ) : visibleCreateView === "template" ? ( setCreateView("menu")} onCreate={onCreate} /> diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index f1037611..b6fd90d2 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -622,6 +622,7 @@ export interface AgentNode { id?: string; name: string; description: string; + instruction?: string; type: AgentNodeType; model: string; tools: string[]; @@ -666,6 +667,8 @@ export interface AgentInfo { model: string; tools: string[]; skills: AgentSkill[]; + /** False when an older Agent Server omits Skill introspection entirely. */ + skillsPreviewSupported: boolean; subAgents: string[]; /** Optional for compatibility with Agent Servers released before this field. */ components?: AgentComponent[]; @@ -673,6 +676,8 @@ export interface AgentInfo { searchSources?: AgentSearchCapability[]; /** Recursive typed tree; only the local server provides it. */ graph?: AgentNode; + /** Complete sanitized builder state exposed by newly generated Agents. */ + draft?: AgentDraft; } export interface SessionCapabilityItem { @@ -877,17 +882,30 @@ async function fetchAgentInfo(app: string, ep: AdkEndpoint): Promise const res = await apiFetch(`/web/agent-info/${app}`, {}, ep); if (!res.ok) throw new Error(`agent-info failed: ${res.status}`); const info = (await res.json()) as Partial; + if (!info.draft) { + try { + const draftRes = await apiFetch(`/web/agent-draft/${app}`, {}, ep); + if (draftRes.ok) { + const payload = (await draftRes.json()) as { draft?: AgentDraft }; + info.draft = payload.draft; + } + } catch { + // Older or non-Studio Agents do not expose editable builder metadata. + } + } return { name: info.name ?? app, description: info.description ?? "", type: info.type, model: info.model ?? "", tools: info.tools ?? [], + skillsPreviewSupported: Array.isArray(info.skills), skills: info.skills ?? [], subAgents: info.subAgents ?? [], components: info.components ?? [], searchSources: info.searchSources ?? [], graph: info.graph, + draft: info.draft, }; } @@ -1081,6 +1099,7 @@ export interface DeployAgentkitResult { runtimeId?: string; consoleUrl?: string; region?: string; + version?: number | null; feishuChannel?: { enabled: boolean; transport: string; @@ -1124,6 +1143,8 @@ export async function deployAgentkitProject( }, opts?: { taskId?: string; + runtimeId?: string; + description?: string; onStage?: (s: DeployStage) => void; im?: { feishu?: { @@ -1161,6 +1182,8 @@ export async function deployAgentkitProject( files, config, taskId, + runtimeId: opts?.runtimeId, + description: opts?.description, im: opts?.im, envs: opts?.envs, }), @@ -1218,6 +1241,7 @@ export async function deployAgentkitProject( runtimeId: final.runtimeId, consoleUrl: final.consoleUrl, region: final.region, + version: final.version, feishuChannel: final.feishuChannel, }; } @@ -1245,6 +1269,7 @@ export interface ManagedRuntime { createdAt: string; author?: string; region: string; + currentVersion?: number | null; } /** List AgentKit runtimes the server authorizes this user to manage. */ @@ -1458,8 +1483,12 @@ export interface CloudRuntime { status: string; region: string; author: string; + createdAt?: string; + currentVersion?: number | null; /** True when this runtime was deployed by the current user (veadk:author). */ isMine: boolean; + /** Server-authorized deletion capability for this managed Runtime. */ + canDelete: boolean; } /** One page of cloud runtimes plus the token to fetch the next page. */ @@ -1554,6 +1583,7 @@ export interface RuntimeDetail { mcpToolsetId: string; artifactUrl: string; artifactType: string; + networkTypes: string[]; } /** Fetch a runtime's control-plane detail (config/status/envs). */ diff --git a/frontend/src/adk/connections.ts b/frontend/src/adk/connections.ts index fb2bc2ea..133fd95f 100644 --- a/frontend/src/adk/connections.ts +++ b/frontend/src/adk/connections.ts @@ -20,6 +20,7 @@ export interface RemoteConnection { * set, `base`/`apiKey` are unused and the apikey stays server-side. */ runtimeId?: string; region?: string; + currentVersion?: number | null; apps: string[]; /** Optional app ID -> friendly name mapping (e.g., "a_1" -> "a_1-4zkzsezc") */ appLabels?: Record; @@ -32,6 +33,11 @@ export interface AgentEntry { app: string; // real ADK app name remote: boolean; host?: string; // remote host, for display + runtimeId?: string; + region?: string; + currentVersion?: number | null; + /** Server-authorized permission for Studio-managed Runtime deletion. */ + canDelete?: boolean; } const STORAGE_KEY = "veadk_agentkit_connections"; @@ -91,6 +97,7 @@ export function addRuntimeConnection( region: string, apps: string[], appLabels?: Record, + currentVersion?: number | null, ): RemoteConnection { const conn: RemoteConnection = { id: `rt_${runtimeId}`, @@ -99,6 +106,7 @@ export function addRuntimeConnection( region, apps, appLabels, + currentVersion, }; const list = [...loadConnections().filter((c) => c.runtimeId !== runtimeId), conn]; persist(list); @@ -111,6 +119,7 @@ export async function connectRuntime( runtimeId: string, name: string, region: string, + currentVersion?: number | null, ): Promise { let apps: string[] | null; try { @@ -126,7 +135,14 @@ export async function connectRuntime( throw new Error("该 Runtime 暂不支持连接,请确认服务已正常运行。"); } const labels = Object.fromEntries(apps.map((app) => [app, name])); - const connection = addRuntimeConnection(runtimeId, name, region, apps, labels); + const connection = addRuntimeConnection( + runtimeId, + name, + region, + apps, + labels, + currentVersion, + ); return remoteAppId(connection.id, apps[0]); } @@ -188,6 +204,9 @@ export function buildAgentEntries( app, remote: true, host: c.runtimeId ? c.name : hostOf(c.base ?? ""), + runtimeId: c.runtimeId, + region: c.region, + currentVersion: c.currentVersion, }; }), ); diff --git a/frontend/src/assets/feishu-logo.svg b/frontend/src/assets/feishu-logo.svg new file mode 100644 index 00000000..4b305987 --- /dev/null +++ b/frontend/src/assets/feishu-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/create/AgentBuildCanvas.css b/frontend/src/create/AgentBuildCanvas.css new file mode 100644 index 00000000..7d46ab13 --- /dev/null +++ b/frontend/src/create/AgentBuildCanvas.css @@ -0,0 +1,732 @@ +.abc-root { + --cw-workbench-toolbar-height: 64px; + --cw-workspace-ink: 222 24% 13%; + --cw-workspace-accent: 162 44% 32%; + --cw-workspace-accent-soft: 156 34% 92%; + --cw-workspace-warm: 42 28% 96%; + flex: 0 1 52%; + min-width: 460px; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border-right: 0; + background: #fff; +} + +.abc-canvas { + flex: 1; + min-height: 0; + background: #fff; +} + +.abc-canvas .react-flow__pane { + cursor: grab; +} + +.abc-canvas .react-flow__pane:active { + cursor: grabbing; +} + +.abc-node { + --abc-type-tone: 220 9% 24%; + --abc-type-soft: 220 10% 97%; + --abc-type-border: 220 9% 78%; + position: relative; + width: 220px; + height: 88px; + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + align-items: center; + gap: 9px; + padding: 12px 14px; + border: 1px solid hsl(var(--abc-type-border)); + border-radius: 13px; + background: hsl(var(--panel)); + box-shadow: 0 10px 30px hsl(var(--foreground) / 0.055); + color: hsl(var(--foreground)); + transition: border-color 150ms ease, box-shadow 150ms ease, transform 150ms ease; +} + +.abc-node:hover { + border-color: hsl(var(--abc-type-tone) / 0.48); + box-shadow: 0 13px 34px hsl(var(--foreground) / 0.08); +} + +.abc-node.is-selected { + border-color: hsl(var(--abc-type-tone) / 0.86); + box-shadow: + 0 0 0 3px hsl(var(--abc-type-tone) / 0.1), + 0 14px 38px hsl(var(--foreground) / 0.09); +} + +.abc-node.is-llm { + grid-template-columns: minmax(0, 1fr); + background: hsl(var(--panel)); +} + +.abc-node.is-a2a { + --abc-type-tone: 213 18% 38%; + --abc-type-soft: 214 20% 94%; + --abc-type-border: 213 15% 72%; + background: + linear-gradient(145deg, hsl(var(--abc-type-soft)), hsl(var(--panel)) 58%); +} + +.abc-canvas .react-flow__node-group { + padding: 0; + border: 0; + border-radius: 18px; + background: transparent; +} + +.abc-group { + --abc-type-tone: 213 40% 40%; + --abc-type-soft: 214 45% 96%; + --abc-type-border: 213 32% 62%; + position: relative; + width: 100%; + height: 100%; + box-sizing: border-box; + overflow: hidden; + border: 1.5px solid hsl(var(--abc-type-border) / 0.72); + border-radius: 18px; + background: + linear-gradient(180deg, hsl(var(--abc-type-soft) / 0.88), transparent 88px), + hsl(var(--panel) / 0.72); + box-shadow: 0 14px 42px hsl(var(--foreground) / 0.055); + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.abc-group.is-selected { + border-color: hsl(var(--abc-type-tone) / 0.86); + box-shadow: + 0 0 0 4px hsl(var(--abc-type-tone) / 0.1), + 0 16px 46px hsl(var(--foreground) / 0.08); +} + +.abc-group.is-parallel { + --abc-type-tone: 40 43% 38%; + --abc-type-soft: 43 52% 94%; + --abc-type-border: 40 38% 58%; + border-style: solid; +} + +.abc-group.is-sequential { + --abc-type-tone: 213 40% 40%; + --abc-type-soft: 214 45% 96%; + --abc-type-border: 213 32% 62%; +} + +.abc-group.is-llm { + --abc-type-tone: 220 9% 24%; + --abc-type-soft: 220 10% 97%; + --abc-type-border: 220 9% 66%; +} + +.abc-group.is-loop { + --abc-type-tone: 151 34% 34%; + --abc-type-soft: 148 32% 94%; + --abc-type-border: 151 28% 55%; +} + +.abc-group-head { + position: relative; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + padding: 10px 132px 10px 72px; + border-bottom: 1px solid hsl(var(--border) / 0.75); +} + +.abc-group-head > span:first-child { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + text-align: center; +} + +.abc-group-head strong { + color: hsl(var(--abc-type-tone)); + font-size: 13px; + letter-spacing: -0.02em; +} + +.abc-group-head small { + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 9.5px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abc-group-head em { + position: absolute; + top: 12px; + right: 22px; + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--background) / 0.82); + color: hsl(var(--muted-foreground)); + font-size: 9px; + font-style: normal; + font-weight: 600; + white-space: nowrap; +} + +.abc-group-add { + height: 40px; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + border: 1px dashed hsl(var(--abc-type-tone) / 0.42); + border-radius: 10px; + background: hsl(var(--background) / 0.58); + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + font-size: 10px; + font-weight: 600; + transition: + border-color 150ms ease, + background-color 150ms ease, + color 150ms ease; +} + +.abc-group-boundary-actions { + position: absolute; + inset: 64px 0 0; + z-index: 2; + pointer-events: none; +} + +.abc-group-boundary-add { + position: absolute; + top: 50%; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid hsl(var(--abc-type-tone) / 0.32); + border-radius: 50%; + background: hsl(var(--background) / 0.94); + box-shadow: 0 6px 18px hsl(var(--foreground) / 0.08); + color: hsl(var(--abc-type-tone)); + cursor: pointer; + opacity: 0.72; + pointer-events: auto; + transform: translateY(-50%); + transition: + border-color 150ms ease, + background-color 150ms ease, + box-shadow 150ms ease, + opacity 150ms ease; +} + +.abc-group-boundary-add.is-start { + left: 18px; +} + +.abc-group-boundary-add.is-end { + right: 18px; +} + +.abc-root.is-vertical .abc-group-boundary-actions { + inset: 64px 0 0; +} + +.abc-root.is-vertical .abc-group-boundary-add { + left: 50%; + transform: translateX(-50%); +} + +.abc-root.is-vertical .abc-group-boundary-add.is-start { + top: 18px; +} + +.abc-root.is-vertical .abc-group-boundary-add.is-end { + top: auto; + right: auto; + bottom: 18px; +} + +.abc-group-boundary-add:hover { + border-color: hsl(var(--abc-type-tone) / 0.64); + background: hsl(var(--abc-type-soft) / 0.92); + box-shadow: 0 8px 22px hsl(var(--foreground) / 0.1); + opacity: 1; +} + +.abc-group-boundary-add:focus-visible { + outline: 2px solid hsl(var(--abc-type-tone) / 0.5); + outline-offset: 2px; + opacity: 1; +} + +.abc-group-boundary-add svg { + width: 14px; + height: 14px; +} + +.abc-group-add-empty, +.abc-group-add-bottom { + position: absolute; + right: 24px; + bottom: 24px; + left: 24px; +} + +.abc-group-add:hover { + border-color: hsl(var(--abc-type-tone) / 0.72); + background: hsl(var(--abc-type-soft) / 0.86); + color: hsl(var(--abc-type-tone)); +} + +.abc-group-add:focus-visible { + outline: 2px solid hsl(var(--abc-type-tone) / 0.5); + outline-offset: 2px; +} + +.abc-group-add svg { + width: 14px; + height: 14px; +} + +.abc-node.is-contained-in-parallel .abc-handle { + opacity: 0; +} + +.abc-node-icon { + width: 38px; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background: hsl(var(--abc-type-soft)); + color: hsl(var(--abc-type-tone)); +} + +.abc-node-icon svg { + width: 17px; + height: 17px; +} + +.abc-node-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + padding-right: 18px; +} + +.abc-node-delete { + position: absolute; + z-index: 3; + top: 7px; + right: 7px; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel) / 0.96); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08); + color: hsl(var(--muted-foreground)); + cursor: pointer; + opacity: 0; + pointer-events: none; + transform: translateY(-2px) scale(0.92); + transition: + opacity 120ms ease, + transform 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.abc-node:hover > .abc-node-delete, +.abc-node:focus-within > .abc-node-delete, +.abc-group:hover > .abc-node-delete, +.abc-group:focus-within > .abc-node-delete, +.abc-node-delete:focus-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); +} + +.abc-node-delete:hover { + border-color: hsl(var(--destructive) / 0.32); + color: hsl(var(--destructive)); +} + +.abc-node-delete:focus-visible { + outline: 2px solid hsl(var(--destructive) / 0.34); + outline-offset: 2px; +} + +.abc-node-delete svg { + width: 12px; + height: 12px; +} + +.abc-group > .abc-node-delete { + top: 19px; + right: 12px; +} + +.abc-group > .abc-node-delete + .abc-handle { + z-index: 4; +} + +.abc-group:has(> .abc-node-delete) .abc-group-head em { + right: 48px; +} + +.abc-loop-handle { + left: 50% !important; + opacity: 0; + pointer-events: none; +} + +.abc-node-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: hsl(var(--abc-type-tone)); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.04em; +} + +.abc-node-meta small { + color: hsl(var(--muted-foreground)); + font-size: 8.5px; + font-weight: 550; + letter-spacing: 0; +} + +.abc-node-copy > strong { + overflow: hidden; + font-size: 13px; + letter-spacing: -0.015em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abc-node-copy > small { + display: -webkit-box; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 9.5px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.abc-terminal { + width: 96px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid hsl(var(--border) / 0.82); + border-radius: 999px; + background: hsl(var(--secondary) / 0.68); + box-shadow: none; + color: hsl(var(--foreground) / 0.76); + font-size: 10.5px; + font-weight: 650; + letter-spacing: 0.03em; +} + +.abc-handle { + width: 7px !important; + height: 7px !important; + border: 2px solid hsl(var(--background)) !important; + background: hsl(220 9% 38%) !important; +} + +.abc-group.is-sequential > .abc-handle { + background: hsl(213 40% 40%) !important; +} + +.abc-group.is-parallel > .abc-handle { + background: hsl(40 43% 38%) !important; +} + +.abc-group.is-loop > .abc-handle, +.abc-node.is-contained-in-loop .abc-loop-handle { + background: hsl(151 34% 34%) !important; +} + +.abc-canvas .react-flow__edge-path { + transition: stroke-width 120ms ease; +} + +.abc-canvas .react-flow__edge:hover .react-flow__edge-path { + stroke-width: 2.2; +} + +.abc-edge-tools { + position: absolute; + z-index: 1002; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 3px; + padding: 0; + border-radius: 999px; + pointer-events: all; +} + +.abc-canvas .react-flow__edgelabel-renderer { + z-index: 1002; +} + +.abc-edge-hover-path { + fill: none; + stroke: transparent; + stroke-width: 22px; + pointer-events: stroke; +} + +.abc-edge-label { + position: absolute; + bottom: calc(100% + 1px); + left: 50%; + padding: 2px 5px; + border-radius: 5px; + background: hsl(var(--background) / 0.92); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 600; + transform: translateX(-50%); + white-space: nowrap; +} + +.abc-edge-add { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid hsl(var(--cw-workspace-accent) / 0.28); + border-radius: 50%; + background: hsl(var(--background)); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.1); + color: hsl(var(--cw-workspace-accent)); + cursor: pointer; + opacity: 0; + transform: scale(0.82); + transition: + opacity 140ms ease, + transform 140ms ease, + border-color 140ms ease; +} + +.abc-edge-tools.is-visible .abc-edge-add, +.abc-edge-tools:hover .abc-edge-add, +.abc-edge-add:focus-visible { + border-color: hsl(var(--cw-workspace-accent) / 0.68); + opacity: 1; + transform: scale(1); +} + +.abc-edge-add:focus-visible { + outline: 2px solid hsl(var(--cw-workspace-accent) / 0.5); + outline-offset: 2px; +} + +.abc-edge-add svg { + width: 11px; + height: 11px; +} + +@media (hover: none) { + .abc-edge-add { + opacity: 0.88; + transform: scale(1); + } + + .abc-node-delete { + opacity: 1; + pointer-events: auto; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .abc-node-delete, + .abc-edge-add { + transition: none; + } +} + +.abc-canvas .react-flow__controls { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 9px; + box-shadow: 0 8px 24px hsl(var(--foreground) / 0.08); +} + +.abc-canvas .react-flow__controls-button { + border-bottom-color: hsl(var(--border)); + background: hsl(var(--panel)); + color: hsl(var(--foreground)); +} + +.abc-minimap { + width: 168px !important; + height: 104px !important; + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 9px; + background: hsl(var(--panel)) !important; + box-shadow: 0 8px 24px hsl(var(--foreground) / 0.06); +} + +.abc-minimap-node .abc-minimap-shell, +.abc-minimap-node .abc-minimap-icon-mark, +.abc-minimap-node .abc-minimap-group-divider { + vector-effect: non-scaling-stroke; +} + +.abc-minimap-node-agent .abc-minimap-shell { + fill: hsl(var(--panel)); + stroke: hsl(220 9% 38% / 0.72); + stroke-width: 1.4px; +} + +.abc-minimap-node-agent.is-a2a .abc-minimap-shell { + fill: hsl(211 48% 96%); + stroke: hsl(202 42% 48% / 0.78); +} + +.abc-minimap-agent-icon { + fill: hsl(214 20% 94%); +} + +.abc-minimap-node-agent.is-a2a .abc-minimap-agent-icon { + fill: hsl(202 46% 90%); +} + +.abc-minimap-icon-mark { + fill: none; + stroke: hsl(213 18% 38%); + stroke-width: 1.25px; +} + +.abc-minimap-icon-eye { + fill: hsl(213 18% 38%); +} + +.abc-minimap-copy-line { + fill: hsl(var(--muted-foreground) / 0.34); +} + +.abc-minimap-copy-line.is-primary { + fill: hsl(var(--foreground) / 0.7); +} + +.abc-minimap-node-terminal .abc-minimap-shell { + fill: hsl(var(--cw-workspace-ink)); + stroke: hsl(var(--panel)); + stroke-width: 1.5px; + vector-effect: non-scaling-stroke; +} + +.abc-minimap-terminal-dot { + fill: hsl(var(--panel) / 0.82); +} + +.abc-minimap-node-group .abc-minimap-shell { + fill: hsl(var(--panel) / 0.32); + stroke: hsl(213 40% 40% / 0.78); + stroke-width: 1.5px; +} + +.abc-minimap-node-group.is-parallel .abc-minimap-shell { + stroke: hsl(40 43% 38% / 0.82); +} + +.abc-minimap-node-group.is-sequential .abc-minimap-shell { + stroke: hsl(213 40% 40% / 0.82); +} + +.abc-minimap-node-group.is-llm .abc-minimap-shell { + stroke: hsl(220 9% 38% / 0.78); +} + +.abc-minimap-node-group.is-loop .abc-minimap-shell { + stroke: hsl(151 34% 34% / 0.84); +} + +.abc-minimap-group-divider { + stroke: hsl(var(--border)); + stroke-width: 1px; +} + +.abc-minimap-group-title { + fill: hsl(var(--muted-foreground) / 0.42); +} + +.abc-minimap-node.is-selected .abc-minimap-shell { + stroke-width: 2.5px; +} + +.abc-minimap-node-agent.is-selected .abc-minimap-shell { + stroke: hsl(220 9% 20%); +} + +.abc-minimap-node-group.is-sequential.is-selected .abc-minimap-shell { + stroke: hsl(213 40% 32%); +} + +.abc-minimap-node-group.is-parallel.is-selected .abc-minimap-shell { + stroke: hsl(40 43% 30%); +} + +.abc-minimap-node-group.is-loop.is-selected .abc-minimap-shell { + stroke: hsl(151 34% 27%); +} + +@media (max-width: 1080px) { + .abc-root { + min-width: 360px; + } + +} + +@media (max-width: 860px) { + .abc-root { + flex: none; + width: 100%; + min-width: 0; + height: 480px; + border-right: 0; + border-bottom: 0; + } + + .abc-minimap { + display: none; + } + +} + +@media (max-width: 520px) { + .abc-root { + height: 430px; + } + +} diff --git a/frontend/src/create/AgentBuildCanvas.tsx b/frontend/src/create/AgentBuildCanvas.tsx new file mode 100644 index 00000000..262e86f6 --- /dev/null +++ b/frontend/src/create/AgentBuildCanvas.tsx @@ -0,0 +1,1181 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Background, + BaseEdge, + Controls, + EdgeLabelRenderer, + getSmoothStepPath, + Handle, + MarkerType, + MiniMap, + Position, + ReactFlow, + ReactFlowProvider, + useEdgesState, + useNodesInitialized, + useNodesState, + useReactFlow, + type Edge, + type EdgeProps, + type MiniMapNodeProps, + type Node, + type NodeProps, +} from "@xyflow/react"; +import dagre from "@dagrejs/dagre"; +import { + ArrowRightLeft, + Bot, + Globe, + ListOrdered, + Plus, + Repeat, + Trash2, +} from "lucide-react"; +import type { AgentDraft } from "./types"; +import "@xyflow/react/dist/style.css"; +import "./AgentBuildCanvas.css"; + +type NodePath = number[]; +type AgentType = NonNullable; + +const PATTERN_COPY: Record< + AgentType, + { label: string; description: string; icon: typeof Bot } +> = { + llm: { + label: "智能体", + description: "理解任务并直接完成一个具体工作", + icon: Bot, + }, + sequential: { + label: "分步协作", + description: "内部步骤按照顺序依次执行", + icon: ListOrdered, + }, + parallel: { + label: "同时处理", + description: "内部步骤同时工作,完成后统一汇总", + icon: ArrowRightLeft, + }, + loop: { + label: "循环执行", + description: "重复执行内部步骤,直到满足停止条件", + icon: Repeat, + }, + a2a: { + label: "远程智能体", + description: "调用已经存在的远程 Agent", + icon: Globe, + }, +}; + +type CanvasNodeData = { + kind: "agent" | "terminal"; + path?: NodePath; + agent?: AgentDraft; + title: string; + pattern?: AgentType; + description?: string; + childCount?: number; + containedIn?: AgentType; + layoutWidth?: number; + layoutHeight?: number; +}; + +type CanvasNode = Node; + +const NODE_WIDTH = 220; +const NODE_HEIGHT = 88; +const TERMINAL_WIDTH = 96; +const TERMINAL_HEIGHT = 34; +const GROUP_HEADER_HEIGHT = 64; +const GROUP_MIN_WIDTH = 310; +const GROUP_PADDING = 24; +const GROUP_BOUNDARY_PADDING = 56; +const GROUP_GAP = 40; +const GROUP_ADD_HEIGHT = 40; +const GROUP_ADD_GAP = 18; +const LOOP_EDGE_SPACE = 58; +const MINIMAP_ENABLED = false; + +const isContainerType = (type: AgentType) => + type === "sequential" || type === "parallel" || type === "loop"; + +function rendersAsGroup(agent: AgentDraft, path: NodePath): boolean { + const type = agent.agentType ?? "llm"; + return ( + isContainerType(type) || + (type === "llm" && (path.length === 0 || agent.subAgents.length > 0)) + ); +} + +export type CanvasDirection = "horizontal" | "vertical"; + +function measureAgent( + agent: AgentDraft, + path: NodePath = [], + direction: CanvasDirection = "horizontal", +): { width: number; height: number } { + const type = agent.agentType ?? "llm"; + if (!rendersAsGroup(agent, path)) { + return { width: NODE_WIDTH, height: NODE_HEIGHT }; + } + const sizes = agent.subAgents.map((child, index) => + measureAgent(child, [...path, index], direction), + ); + const widestChild = sizes.length + ? Math.max(...sizes.map((size) => size.width)) + : 0; + const tallestChild = sizes.length + ? Math.max(...sizes.map((size) => size.height)) + : 0; + const flowPadding = sizes.length && type !== "parallel" + ? GROUP_BOUNDARY_PADDING + : GROUP_PADDING; + const horizontalChildren = direction === "horizontal" + ? type !== "parallel" + : type === "parallel"; + const bottomSpace = sizes.length + ? type === "parallel" + ? GROUP_ADD_GAP + GROUP_ADD_HEIGHT + : type === "loop" + ? LOOP_EDGE_SPACE + : 0 + : GROUP_ADD_HEIGHT; + if (!horizontalChildren) { + return { + width: Math.max( + GROUP_MIN_WIDTH, + widestChild + GROUP_PADDING * 2, + ), + height: + GROUP_HEADER_HEIGHT + + flowPadding + + sizes.reduce((sum, size) => sum + size.height, 0) + + GROUP_GAP * Math.max(0, sizes.length - 1) + + bottomSpace + + flowPadding, + }; + } + return { + width: Math.max( + GROUP_MIN_WIDTH, + sizes.reduce((sum, size) => sum + size.width, 0) + + GROUP_GAP * Math.max(0, sizes.length - 1) + + flowPadding * 2, + ), + height: + GROUP_HEADER_HEIGHT + + GROUP_PADDING + + tallestChild + + bottomSpace + + GROUP_PADDING, + }; +} + +function pathId(path: NodePath): string { + return path.length === 0 ? "agent-root" : `agent-${path.join("-")}`; +} + +function samePath(a: NodePath, b: NodePath): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} + +function structureKey(root: AgentDraft): string { + const visit = (node: AgentDraft): unknown => [ + node.agentType ?? "llm", + node.subAgents.map(visit), + ]; + return JSON.stringify(visit(root)); +} + +type CanvasEdgeData = { + insert?: { parentPath: NodePath; index: number }; + loop?: boolean; + tone?: AgentType; +}; + +type CanvasEdge = Edge; + +function makeEdge( + source: string, + target: string, + label?: string, + options?: { + loop?: boolean; + tone?: AgentType; + insert?: CanvasEdgeData["insert"]; + }, +): CanvasEdge { + const edgeColor = + options?.tone === "sequential" + ? "hsl(213 40% 40%)" + : options?.tone === "loop" + ? "hsl(151 34% 34%)" + : "hsl(220 9% 38%)"; + return { + id: `${source}-${target}${options?.loop ? "-loop" : ""}`, + source, + target, + sourceHandle: options?.loop ? "loop-source" : undefined, + targetHandle: options?.loop ? "loop-target" : undefined, + label, + type: "insertStep", + data: options + ? { + insert: options.insert, + loop: options.loop, + tone: options.tone, + } + : undefined, + animated: options?.loop, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + color: edgeColor, + }, + style: { + stroke: edgeColor, + strokeWidth: 1.5, + }, + labelStyle: { + fill: "hsl(215 14% 42%)", + fontSize: 10, + fontWeight: 600, + }, + labelBgStyle: { + fill: "hsl(var(--background))", + fillOpacity: 0.92, + }, + }; +} + +function buildCanvasGraph( + root: AgentDraft, + direction: CanvasDirection, +): { + nodes: CanvasNode[]; + edges: CanvasEdge[]; +} { + const nodes: CanvasNode[] = [ + { + id: "terminal-input", + type: "terminal", + position: { x: 0, y: 0 }, + data: { kind: "terminal", title: "用户请求" }, + selectable: false, + draggable: false, + }, + { + id: "terminal-output", + type: "terminal", + position: { x: 0, y: 0 }, + data: { kind: "terminal", title: "最终回复" }, + selectable: false, + draggable: false, + }, + ]; + const edges: CanvasEdge[] = []; + + function addContainedNode( + agent: AgentDraft, + path: NodePath, + parentId: string, + position: { x: number; y: number }, + containedIn: AgentType, + ): string { + const type = agent.agentType ?? "llm"; + const id = pathId(path); + if (rendersAsGroup(agent, path)) { + addGroupNode(agent, path, parentId, position, containedIn); + return id; + } + nodes.push({ + id, + type: "agent", + parentId, + extent: "parent", + position, + data: { + kind: "agent", + path, + agent, + title: + type === "a2a" + ? "远程智能体" + : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + pattern: type, + description: agent.description.trim() || PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + containedIn, + }, + }); + return id; + } + + function addGroupNode( + agent: AgentDraft, + path: NodePath, + parentId?: string, + position = { x: 0, y: 0 }, + containedIn?: AgentType, + ): string { + const type = agent.agentType ?? "sequential"; + const id = pathId(path); + const size = measureAgent(agent, path, direction); + nodes.push({ + id, + type: "group", + parentId, + extent: parentId ? "parent" : undefined, + position, + style: { width: size.width, height: size.height }, + data: { + kind: "agent", + path, + agent, + title: + agent.name.trim() || + (path.length === 0 ? "主 Agent" : PATTERN_COPY[type].label), + pattern: type, + description: PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + containedIn, + layoutWidth: size.width, + layoutHeight: size.height, + }, + }); + + const childSizes = agent.subAgents.map((child, index) => + measureAgent(child, [...path, index], direction), + ); + const flowPadding = childSizes.length && type !== "parallel" + ? GROUP_BOUNDARY_PADDING + : GROUP_PADDING; + const horizontalChildren = direction === "horizontal" + ? type !== "parallel" + : type === "parallel"; + let cursor = flowPadding; + const childIds = agent.subAgents.map((child, index) => { + const childSize = childSizes[index]; + const childPosition = + horizontalChildren + ? { + x: cursor, + y: GROUP_HEADER_HEIGHT + GROUP_PADDING, + } + : { + x: (size.width - childSize.width) / 2, + y: GROUP_HEADER_HEIGHT + cursor, + }; + cursor += + (horizontalChildren ? childSize.width : childSize.height) + GROUP_GAP; + return addContainedNode( + child, + [...path, index], + id, + childPosition, + type, + ); + }); + + if (type === "sequential" || type === "loop") { + for (let index = 0; index < childIds.length - 1; index += 1) { + edges.push( + makeEdge(childIds[index], childIds[index + 1], "然后", { + tone: type, + insert: { parentPath: path, index: index + 1 }, + }), + ); + } + if (type === "loop" && childIds.length > 1) { + edges.push( + makeEdge(childIds[childIds.length - 1], childIds[0], "继续循环", { + loop: true, + tone: "loop", + }), + ); + } + } + return id; + } + + const addTopLevelNode = (agent: AgentDraft, path: NodePath): string[] => { + const type = agent.agentType ?? "llm"; + const id = pathId(path); + if (rendersAsGroup(agent, path)) { + addGroupNode(agent, path); + return [id]; + } + nodes.push({ + id, + type: "agent", + position: { x: 0, y: 0 }, + data: { + kind: "agent", + path, + agent, + title: + type === "a2a" + ? "远程智能体" + : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + pattern: type, + description: agent.description.trim() || PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + }, + }); + if (agent.subAgents.length === 0) return [id]; + + const exits: string[] = []; + agent.subAgents.forEach((child, index) => { + const childPath = [...path, index]; + const childId = pathId(childPath); + edges.push( + makeEdge(id, childId, "调用", { + insert: { parentPath: path, index }, + }), + ); + exits.push(...addTopLevelNode(child, childPath)); + }); + return exits; + }; + + const rootId = pathId([]); + const exits = addTopLevelNode(root, []); + edges.push(makeEdge("terminal-input", rootId)); + exits.forEach((exitId) => + edges.push(makeEdge(exitId, "terminal-output")), + ); + return layoutGraph(nodes, edges, direction); +} + +function layoutGraph( + nodes: CanvasNode[], + edges: CanvasEdge[], + direction: CanvasDirection, +) { + const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + graph.setGraph({ + rankdir: direction === "vertical" ? "TB" : "LR", + ranksep: 50, + nodesep: 34, + edgesep: 14, + marginx: 24, + marginy: 24, + }); + const topLevelIds = new Set( + nodes.filter((node) => !node.parentId).map((node) => node.id), + ); + nodes.filter((node) => !node.parentId).forEach((node) => { + const terminal = node.data.kind === "terminal"; + graph.setNode(node.id, { + width: terminal + ? TERMINAL_WIDTH + : node.data.layoutWidth ?? NODE_WIDTH, + height: terminal + ? TERMINAL_HEIGHT + : node.data.layoutHeight ?? NODE_HEIGHT, + }); + }); + edges + .filter( + (edge) => topLevelIds.has(edge.source) && topLevelIds.has(edge.target), + ) + .forEach((edge) => graph.setEdge(edge.source, edge.target)); + dagre.layout(graph); + return { + nodes: nodes.map((node) => { + if (node.parentId) return node; + const position = graph.node(node.id) as { x: number; y: number }; + const terminal = node.data.kind === "terminal"; + const width = terminal + ? TERMINAL_WIDTH + : node.data.layoutWidth ?? NODE_WIDTH; + const height = terminal + ? TERMINAL_HEIGHT + : node.data.layoutHeight ?? NODE_HEIGHT; + return { + ...node, + position: { x: position.x - width / 2, y: position.y - height / 2 }, + }; + }), + edges, + }; +} + +type CanvasActions = { + onAdd: (path: NodePath) => void; + onInsert: (parentPath: NodePath, index: number) => void; + onDelete: (path: NodePath) => void; +}; + +const CanvasActionsContext = createContext(null); +const CanvasDirectionContext = createContext("horizontal"); + +function InsertStepEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + markerEnd, + style, + label, + data, +}: EdgeProps) { + const actions = useContext(CanvasActionsContext); + const [showInsert, setShowInsert] = useState(false); + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + offset: data?.loop ? 28 : 20, + }); + return ( + <> + + {actions && data?.insert && ( + setShowInsert(true)} + onPointerLeave={() => setShowInsert(false)} + /> + )} + {(label || (actions && data?.insert)) && ( + +
setShowInsert(true)} + onPointerLeave={() => setShowInsert(false)} + > + {label && {label}} + {actions && data?.insert && ( + + )} +
+
+ )} + + ); +} + +function AgentCanvasNode({ data, selected }: NodeProps) { + const actions = useContext(CanvasActionsContext); + const direction = useContext(CanvasDirectionContext); + const targetPosition = direction === "vertical" ? Position.Top : Position.Left; + const sourcePosition = direction === "vertical" ? Position.Bottom : Position.Right; + const loopPosition = direction === "vertical" ? Position.Right : Position.Bottom; + const type = data.pattern ?? "llm"; + const copy = PATTERN_COPY[type]; + const Icon = copy.icon; + return ( +
+ + {type !== "llm" && ( + + )} + + + {copy.label} + {!!data.childCount && {data.childCount} 个步骤} + + {data.title} + {data.description} + + {actions && data.path !== undefined && data.path.length > 0 && ( + + )} + + {data.containedIn === "loop" && ( + <> + + + + )} +
+ ); +} + +function AgentGroupNode({ data, selected }: NodeProps) { + const actions = useContext(CanvasActionsContext); + const direction = useContext(CanvasDirectionContext); + const targetPosition = direction === "vertical" ? Position.Top : Position.Left; + const sourcePosition = direction === "vertical" ? Position.Bottom : Position.Right; + const loopPosition = direction === "vertical" ? Position.Right : Position.Bottom; + const type = data.pattern ?? "sequential"; + const copy = PATTERN_COPY[type]; + const childUnit = type === "llm" ? "子 Agent" : "步骤"; + const childCount = data.childCount ?? 0; + const addLabel = + type === "llm" + ? "添加子 Agent" + : type === "parallel" + ? "添加一个同时处理的步骤" + : type === "loop" + ? "添加循环步骤" + : "添加下一个步骤"; + return ( +
+ +
+ + {data.title} + + {type === "llm" + ? "智能体 · 可根据任务调用框内子 Agent" + : type === "parallel" + ? `${copy.label} · 框内步骤同时开始,全部完成后再继续` + : `${copy.label} · ${copy.description}`} + + + {childCount} 个{childUnit} +
+ {actions && + data.path !== undefined && + childCount > 0 && + type !== "parallel" && ( +
+ + +
+ )} + {actions && + data.path !== undefined && + childCount > 0 && + type === "parallel" && ( + + )} + {actions && data.path !== undefined && childCount === 0 && ( + + )} + {actions && data.path !== undefined && data.path.length > 0 && ( + + )} + + {data.containedIn === "loop" && ( + <> + + + + )} +
+ ); +} + +function TerminalNode({ data }: NodeProps) { + const direction = useContext(CanvasDirectionContext); + return ( +
+ + {data.title} + +
+ ); +} + +const NODE_TYPES = { + agent: AgentCanvasNode, + group: AgentGroupNode, + terminal: TerminalNode, +}; + +const EDGE_TYPES = { + insertStep: InsertStepEdge, +}; + +function CanvasMiniMapNode({ + id, + x, + y, + width, + height, + className, + selected, + onClick, +}: MiniMapNodeProps) { + const isGroup = className.includes("abc-minimap-node-group"); + const isTerminal = className.includes("abc-minimap-node-terminal"); + const isLlm = className.includes("is-llm"); + const isExternal = className.includes("is-a2a"); + const radius = Math.min(18, height / 4); + const nodeClassName = `${className}${selected ? " is-selected" : ""}`; + + if (isTerminal) { + return ( + onClick(event, id) : undefined} + > + + + + ); + } + + if (isGroup) { + const headerHeight = Math.min(42, Math.max(18, height * 0.22)); + return ( + onClick(event, id) : undefined} + > + + + + + ); + } + + const iconSize = isLlm ? 0 : Math.min(36, height * 0.5, width * 0.18); + const sidePadding = Math.max(10, width * 0.08); + const iconX = x + sidePadding; + const iconY = y + (height - iconSize) / 2; + const textX = isLlm + ? x + sidePadding + : iconX + iconSize + Math.max(8, width * 0.05); + const textWidth = Math.max(10, x + width - textX - sidePadding); + const lineHeight = Math.max(4, height * 0.08); + + return ( + onClick(event, id) : undefined} + > + + {!isLlm && } + {!isLlm && (isExternal ? ( + <> + + + + ) : ( + <> + + + + + ))} + + + + ); +} + +export interface AgentBuildCanvasProps { + draft: AgentDraft; + selectedPath: NodePath; + onSelect: (path: NodePath) => void; + onAdd: (path: NodePath) => void; + onInsert: (parentPath: NodePath, index: number) => void; + onDelete: (path: NodePath) => void; + /** Show the graph without any structure-changing actions. */ + readOnly?: boolean; + /** Allow pan and zoom while remaining read-only. */ + interactivePreview?: boolean; + /** Lay out the workflow from left to right or top to bottom. */ + direction?: CanvasDirection; +} + +function AgentBuildCanvasInner({ + draft, + selectedPath, + onSelect, + onAdd, + onInsert, + onDelete, + readOnly = false, + interactivePreview = false, + direction = "horizontal", +}: AgentBuildCanvasProps) { + const initialGraph = useMemo(() => buildCanvasGraph(draft, direction), []); + const [nodes, setNodes, onNodesChange] = useNodesState( + initialGraph.nodes, + ); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialGraph.edges); + const nodesInitialized = useNodesInitialized(); + const lastStructure = useRef(`${direction}:${structureKey(draft)}`); + const canvasRef = useRef(null); + const { fitView } = useReactFlow(); + const currentGraph = useMemo( + () => buildCanvasGraph(draft, direction), + [direction, draft], + ); + const [compactCanvas, setCompactCanvas] = useState(() => + window.matchMedia("(max-width: 860px)").matches, + ); + const fitOptions = useMemo( + () => + readOnly + ? { padding: 0.16, minZoom: 0.05, maxZoom: 0.9 } + : compactCanvas + ? { padding: 0.08, minZoom: 0.35, maxZoom: 0.9 } + : { padding: 0.14, minZoom: 0.42, maxZoom: 1.1 }, + [compactCanvas, readOnly], + ); + const fitAfterLayout = useCallback(() => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => void fitView(fitOptions)); + }); + }, [fitOptions, fitView]); + + useEffect(() => { + const query = window.matchMedia("(max-width: 860px)"); + const handleChange = (event: MediaQueryListEvent) => + setCompactCanvas(event.matches); + query.addEventListener("change", handleChange); + return () => query.removeEventListener("change", handleChange); + }, []); + + useEffect(() => { + const nextStructure = `${direction}:${structureKey(draft)}`; + const structureChanged = nextStructure !== lastStructure.current; + lastStructure.current = nextStructure; + setEdges(currentGraph.edges); + setNodes((current) => { + const currentPositions = new Map( + current.map((node) => [node.id, node.position] as const), + ); + return currentGraph.nodes.map((node) => ({ + ...node, + position: + !structureChanged && currentPositions.get(node.id) + ? currentPositions.get(node.id)! + : node.position, + selected: + node.data.kind === "agent" && + !!node.data.path && + samePath(node.data.path, selectedPath), + })); + }); + if (structureChanged) { + fitAfterLayout(); + } + }, [currentGraph, draft, fitAfterLayout, selectedPath, setEdges, setNodes]); + + useEffect(() => { + fitAfterLayout(); + }, [compactCanvas, fitAfterLayout]); + + useEffect(() => { + if (!nodesInitialized) return; + fitAfterLayout(); + }, [currentGraph, fitAfterLayout, nodesInitialized]); + + useEffect(() => { + if (!readOnly || !canvasRef.current) return; + const observer = new ResizeObserver(() => fitAfterLayout()); + observer.observe(canvasRef.current); + fitAfterLayout(); + return () => observer.disconnect(); + }, [fitAfterLayout, readOnly]); + + const canvasActions = useMemo( + () => + readOnly ? null : { onAdd, onInsert, onDelete }, + [onAdd, onDelete, onInsert, readOnly], + ); + return ( + + +
+
+ + nodes={nodes} + edges={edges} + nodeTypes={NODE_TYPES} + edgeTypes={EDGE_TYPES} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onNodeClick={(_, node) => { + if (!readOnly && node.data.kind === "agent" && node.data.path) { + onSelect(node.data.path); + } + }} + nodesDraggable={!readOnly} + nodesConnectable={false} + nodesFocusable={!readOnly} + elementsSelectable={!readOnly} + edgesFocusable={false} + edgesReconnectable={false} + panOnDrag={!readOnly || interactivePreview} + zoomOnDoubleClick={interactivePreview} + zoomOnPinch={!readOnly || interactivePreview} + zoomOnScroll={!readOnly || interactivePreview} + fitView + fitViewOptions={fitOptions} + minZoom={readOnly ? 0.05 : 0.35} + maxZoom={1.6} + proOptions={{ hideAttribution: true }} + > + + {(!readOnly || interactivePreview) && ( + + )} + {MINIMAP_ENABLED && ( + + [ + "abc-minimap-node", + node.type === "group" + ? "abc-minimap-node-group" + : node.data.kind === "terminal" + ? "abc-minimap-node-terminal" + : "abc-minimap-node-agent", + node.data.pattern ? `is-${node.data.pattern}` : "", + ] + .filter(Boolean) + .join(" ") + } + /> + )} + +
+
+
+
+ ); +} + +export function AgentBuildCanvas(props: AgentBuildCanvasProps) { + return ( + + + + ); +} diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index b3c0531f..79fd96ea 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -3,22 +3,218 @@ * Uses the shadcn CSS variables from src/styles.css :root. */ .cw-root { + --cw-workspace-gutter: 12px; --cw-workbench-toolbar-height: 64px; + --cw-workspace-ink: 222 24% 13%; + --cw-workspace-accent: 162 44% 32%; + --cw-workspace-accent-soft: 156 34% 92%; + --cw-workspace-warm: 42 28% 96%; flex: 1; min-height: 0; display: flex; flex-direction: column; height: 100%; color: hsl(var(--foreground)); + background: #fff; +} + +/* ---------- workspace lifecycle navigation ---------- */ +.cw-workspace-header { + position: relative; + z-index: 12; + flex: 0 0 auto; + min-height: 56px; + display: grid; + grid-template-columns: minmax(180px, 1fr) auto minmax(180px, 1fr); + align-items: center; + gap: 16px; + margin: 8px var(--cw-workspace-gutter) 0; + padding: 6px 14px; + border: 0; + border-radius: 16px; + background: rgba(246, 246, 248, 0.82); + box-shadow: none; + -webkit-backdrop-filter: blur(7px); + backdrop-filter: blur(7px); +} +.cw-workspace-identity { + min-width: 0; +} +.cw-workspace-identity > strong { + min-width: 0; + overflow: hidden; + color: hsl(var(--cw-workspace-ink)); + font-size: 15px; + font-weight: 700; + letter-spacing: -0.025em; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-workspace-actions { + grid-column: 3; + justify-self: end; +} +.cw-discard-edit { + padding: 7px 10px; + border: 0; + border-radius: 8px; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + font-size: 12.5px; + font-weight: 560; + transition: background-color 150ms ease, color 150ms ease; +} +.cw-discard-edit:hover:not(:disabled) { + background: hsl(var(--destructive) / 0.08); + color: hsl(var(--destructive)); +} +.cw-discard-edit:focus-visible { + outline: 2px solid hsl(var(--destructive) / 0.22); + outline-offset: 2px; +} +.cw-discard-edit:disabled { + cursor: wait; + opacity: 0.48; +} +.cw-workspace-stepper { + grid-column: 2; + width: max-content; + max-width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} +.cw-workspace-stepper button { + position: relative; + z-index: 1; + min-width: 124px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 0 14px; + border: 0; + border-radius: 10px; + background: rgba(237, 237, 241, 0.78); + color: hsl(0 0% 28%); + cursor: pointer; + font: inherit; + text-align: center; + transition: + background-color 180ms cubic-bezier(0.22, 1, 0.36, 1), + color 150ms ease, + box-shadow 180ms ease; +} +.cw-workspace-stepper button:not(:last-child)::after { + content: none; +} +.cw-workspace-stepper button:hover:not(:disabled), +.cw-workspace-stepper button.is-complete { + color: hsl(var(--cw-workspace-ink)); +} +.cw-workspace-stepper button:hover:not(:disabled) { + background: rgba(228, 228, 233, 0.84); +} +.cw-workspace-stepper button.is-active { + background: rgba(218, 218, 224, 0.86); + color: hsl(0 0% 10%); + box-shadow: none; +} +.cw-workspace-stepper button:focus-visible { + outline: none; +} +.cw-workspace-stepper button:focus-visible .cw-workspace-step-marker { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.28); + outline-offset: 3px; +} +.cw-workspace-stepper button:disabled { + cursor: wait; + opacity: 0.62; +} +.cw-workspace-step-marker { + width: 20px; + height: 20px; + flex: 0 0 20px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 50%; + background: hsl(var(--foreground) / 0.06); + color: hsl(var(--muted-foreground)); + box-shadow: none; + font-size: 10.5px; + font-weight: 650; + transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease; +} +.cw-workspace-stepper button:hover:not(:disabled) .cw-workspace-step-marker { + background: hsl(var(--foreground) / 0.1); +} +.cw-workspace-stepper button.is-active .cw-workspace-step-marker, +.cw-workspace-stepper button.is-complete .cw-workspace-step-marker { + border: 0; +} +.cw-workspace-stepper button.is-active .cw-workspace-step-marker { + background: rgba(255, 255, 255, 0.5); + color: hsl(0 0% 18%); +} +.cw-workspace-stepper button.is-complete .cw-workspace-step-marker { + background: rgba(255, 255, 255, 0.58); + color: hsl(0 0% 18%); +} +.cw-workspace-step-marker .cw-i { + width: 13px; + height: 13px; +} +.cw-workspace-stepper button > strong { + font-size: 14px; + font-weight: 650; +} +@media (prefers-reduced-motion: reduce) { + .cw-workspace-stepper button, + .cw-workspace-step-marker { + transition: none; + } +} +.cw-workspace-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + overflow: hidden; + background: #fff; +} +.cw-workspace-alert { + position: absolute; + z-index: 30; + top: 94px; + right: 18px; + max-width: min(420px, calc(100% - 36px)); + padding: 10px 13px; + border: 1px solid hsl(var(--destructive) / 0.2); + border-radius: 9px; + background: hsl(var(--background)); + box-shadow: 0 12px 36px hsl(var(--foreground) / 0.12); + color: hsl(var(--destructive)); + font-size: 12.5px; } /* ---------- master-detail editor: left tree + right form ---------- */ .cw-editor { flex: 1; + width: 100%; min-height: 0; display: flex; align-items: stretch; } +.cw-editor > .abc-root { + flex-basis: 42%; + min-width: 380px; +} /* Left: the Agent structure tree. */ .cw-tree { @@ -140,29 +336,15 @@ /* Right side: a fixed agent-type bar on top + a scrolling form/nav area. */ .cw-detail { - flex: 1; + position: relative; + flex: 1 1 58%; + width: auto; + max-width: 780px; min-width: 0; min-height: 0; display: flex; flex-direction: column; } -/* Fixed top bar (outside the scroll area) — never scrolls. */ -.cw-typebar { - position: relative; - z-index: 5; - flex-shrink: 0; - height: var(--cw-workbench-toolbar-height); - background: hsl(var(--panel)); - border-bottom: 1px solid hsl(var(--border)); - padding: 10px 16px; -} -.cw-typebar-inner { - height: 100%; - display: flex; - align-items: center; - max-width: 1080px; - margin: 0 auto; -} /* The scrolling region below the type bar. */ .cw-detail-scroll { flex: 1; @@ -171,7 +353,24 @@ scrollbar-gutter: stable both-edges; /* Left padding matches the tree's gap-from-divider so the form mirrors the Agent-structure items across the divider. */ - padding: 24px 16px 48px; + padding: 24px 16px 96px; +} +.cw-build-next { + position: absolute; + right: auto; + bottom: 20px; + left: 50%; + z-index: 8; + transform: translateX(-50%); +} +.cw-build-next.studio-update-action { + background: #111; + color: #fff; +} +.cw-build-next.studio-update-action:not(:disabled):hover { + background: #29292b; + box-shadow: 0 7px 18px hsl(0 0% 0% / 0.16); + transform: translateX(-50%); } .cw-detail-inner { max-width: 780px; @@ -244,11 +443,6 @@ padding: 10px 16px; border-bottom: 1px solid hsl(var(--border)); } -.cw-debug-head-actions { - display: flex; - align-items: center; - gap: 6px; -} .cw-debug-collapse, .cw-debug-expand { display: inline-flex; @@ -297,7 +491,6 @@ font-weight: 650; color: hsl(var(--foreground)); } -.cw-debug-deploy, .cw-debug-start { display: inline-flex; align-items: center; @@ -307,45 +500,25 @@ padding: 6px 10px; border: none; border-radius: 8px; - background: hsl(var(--primary)); - color: hsl(var(--primary-foreground)); + background: #111; + box-shadow: none; + color: #fff; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: - opacity 0.12s, - transform 0.1s; -} -.cw-debug-deploy { - gap: 4px; - font-size: 13px; -} -.cw-debug-deploy .cw-i { - width: 13px; - height: 13px; - transition: transform 0.18s ease; -} -.cw-debug-deploy:hover:not(:disabled) .cw-i:not(.cw-spin) { - transform: translateX(2px); -} -@media (prefers-reduced-motion: reduce) { - .cw-debug-deploy .cw-i { - transition: none; - } - .cw-debug-deploy:hover:not(:disabled) .cw-i:not(.cw-spin) { - transform: none; - } + background-color 180ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 180ms ease, + transform 150ms ease; } -.cw-debug-deploy:hover:not(:disabled), .cw-debug-start:hover:not(:disabled) { - opacity: 0.86; + background: #29292b; + box-shadow: 0 7px 18px hsl(0 0% 0% / 0.16); } -.cw-debug-deploy:active:not(:disabled), .cw-debug-start:active:not(:disabled) { - transform: scale(0.96); + transform: translateY(0) scale(0.98); } -.cw-debug-deploy:disabled, .cw-debug-start:disabled { opacity: 0.45; cursor: default; @@ -360,15 +533,6 @@ font-size: 12px; line-height: 1.45; } -.cw-debug-deploy-error { - margin: 0 16px 12px; - padding: 8px 10px; - border-radius: 8px; - background: hsl(var(--destructive) / 0.08); - color: hsl(var(--destructive)); - font-size: 12px; - line-height: 1.45; -} .cw-debug-stage { position: relative; flex: 1; @@ -398,9 +562,6 @@ line-height: 1.6; text-align: center; } -.cw-debug-start { - color: hsl(var(--primary-foreground)); -} .cw-debug-run-icon { width: 17px; height: 17px; @@ -501,145 +662,1102 @@ border-radius: 18px; background: hsl(var(--secondary)); } -.cw-debug-msg-assistant .cw-debug-content { - width: 100%; +.cw-debug-msg-assistant .cw-debug-content { + width: 100%; +} +.cw-debug-composer { + flex-shrink: 0; + padding: 10px 14px 14px; + background: hsl(var(--panel)); +} +.cw-debug-composerbox { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 6px 6px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 24px; + background: hsl(var(--background)); +} +.cw-debug-input { + flex: 1; + min-width: 0; + max-height: 120px; + padding: 8px 4px; + border: none; + outline: none; + resize: none; + overflow-y: auto; + background: transparent; + color: hsl(var(--foreground)); + font: inherit; + font-size: 14px; + line-height: 1.5; +} +.cw-debug-input::placeholder { + color: hsl(var(--muted-foreground)); +} +.cw-debug-send { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + cursor: pointer; + transition: + opacity 0.15s, + transform 0.1s, + background 0.12s, + color 0.12s; +} +.cw-debug-send { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} +.cw-debug-send:hover:not(:disabled) { + opacity: 0.85; +} +.cw-debug-send:active:not(:disabled) { + transform: scale(0.94); +} +.cw-debug-send:disabled { + opacity: 0.3; + cursor: default; +} +.cw-debug-overlay { + position: absolute; + z-index: 5; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 22px; + background: hsl(var(--panel) / 0.5); + backdrop-filter: blur(9px) saturate(0.9); + -webkit-backdrop-filter: blur(9px) saturate(0.9); +} +.cw-debug-overlay-content { + width: min(100%, 290px); + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 18px; + border: 1px solid hsl(var(--border) / 0.72); + border-radius: 12px; + background: hsl(var(--background) / 0.82); + box-shadow: 0 10px 30px hsl(var(--foreground) / 0.08); + text-align: center; +} +.cw-debug-overlay-title { + color: hsl(var(--foreground)); + font-size: 14px; + font-weight: 650; +} +.cw-debug-overlay-copy { + color: hsl(var(--muted-foreground)); + font-size: 12.5px; + line-height: 1.55; +} +.cw-debug-overlay-progress { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; +} +.cw-debug-overlay-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 2px; +} +.cw-debug-ignore { + min-height: 30px; + padding: 6px 11px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background) / 0.72); + color: hsl(var(--foreground)); + font: inherit; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: + background 0.12s, + border-color 0.12s, + transform 0.1s; +} +.cw-debug-ignore:hover:not(:disabled) { + border-color: hsl(var(--foreground) / 0.2); + background: hsl(var(--background)); +} +.cw-debug-ignore:active:not(:disabled) { + transform: scale(0.96); +} +.cw-debug-ignore:disabled { + opacity: 0.45; + cursor: default; +} + +/* ---------- validation workspace ---------- */ +.cw-validation-workspace { + position: relative; + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + background: hsl(var(--background)); +} +.cw-optimization-panel { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + padding: 22px 16px 16px; + border-right: 1px solid hsl(var(--border)); + background: hsl(var(--cw-workspace-warm)); +} +.cw-optimization-head { + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 4px 18px; +} +.cw-optimization-head > span { + color: hsl(var(--cw-workspace-ink)); + font-size: 16px; + font-weight: 700; + letter-spacing: -0.02em; +} +.cw-optimization-head > small { + color: hsl(var(--muted-foreground)); + font-size: 11px; + line-height: 1.45; +} +.cw-optimization-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.cw-optimization-option { + position: relative; + width: 100%; + display: flex; + align-items: flex-start; + gap: 9px; + padding: 11px; + border: 1px solid hsl(var(--border) / 0.82); + border-radius: 11px; + background: hsl(var(--panel) / 0.62); + color: hsl(var(--foreground)); + cursor: pointer; + transition: + background-color 140ms ease, + border-color 140ms ease, + box-shadow 140ms ease; +} +.cw-optimization-option:hover { + border-color: hsl(var(--cw-workspace-ink) / 0.24); + background: hsl(var(--panel)); +} +.cw-optimization-option.is-disabled { + cursor: not-allowed; + opacity: 0.62; +} +.cw-optimization-option.is-disabled:hover { + border-color: hsl(var(--border) / 0.82); + background: hsl(var(--panel) / 0.62); +} +.cw-optimization-option:has(input:focus-visible) { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.34); + outline-offset: 2px; +} +.cw-optimization-option input { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + opacity: 0; + pointer-events: none; +} +.cw-optimization-check { + width: 17px; + height: 17px; + flex: 0 0 17px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 1px; + border: 1px solid hsl(var(--foreground) / 0.24); + border-radius: 5px; + background: hsl(var(--panel)); + color: white; +} +.cw-optimization-check .cw-i { + width: 11px; + height: 11px; + stroke-width: 2.4; +} +.cw-optimization-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.cw-optimization-copy strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 12.5px; + font-weight: 650; +} +.cw-optimization-copy small { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 1.45; +} +.cw-validation-content { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + overflow: hidden; + background: #fff; +} +.cw-ab-workspace { + flex: 1; + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + overflow: hidden; + background: #fff; +} +.cw-ab-stage { + position: relative; + flex: 1; + min-width: 0; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 8px var(--cw-workspace-gutter); +} +.cw-ab-grid { + min-height: 100%; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-auto-rows: minmax(420px, 1fr); + align-items: stretch; + gap: 12px; +} +.cw-ab-add { + min-height: 420px; + border: 1px dashed hsl(var(--foreground) / 0.2); + border-radius: 16px; + background: hsl(var(--background) / 0.5); +} +.cw-ab-card { + min-width: 0; + min-height: 420px; + display: flex; + flex-direction: column; + perspective: 1400px; +} +.cw-ab-card-inner { + position: relative; + width: 100%; + min-height: 420px; + flex: 1; + transform-style: preserve-3d; + transition: transform 440ms cubic-bezier(0.22, 1, 0.36, 1); +} +.cw-ab-card-inner.is-flipped { + transform: rotateY(180deg); +} +.cw-ab-card-face { + position: absolute; + inset: 0; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px dashed hsl(var(--foreground) / 0.2); + border-radius: 16px; + background: hsl(var(--background)); + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + transition: border-color 160ms ease, background-color 160ms ease; +} +.cw-ab-card-back { + transform: rotateY(180deg); + overflow-x: hidden; + overflow-y: auto; +} +.cw-ab-card-head { + min-height: 54px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 9px 11px 9px 14px; +} +.cw-ab-card-title { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.cw-ab-card-title strong { + font-size: 13.5px; + font-weight: 680; +} +.cw-ab-card-title span { + max-width: 150px; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-ab-card-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 4px; +} +.cw-ab-config-trigger, +.cw-ab-remove { + min-height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0 8px; + border: 0; + border-radius: 7px; + background: hsl(var(--secondary) / 0.58); + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + font-size: 11px; + font-weight: 400; +} +.cw-ab-config-trigger { + background: hsl(45 92% 90%); + color: hsl(37 70% 30%); +} +.cw-ab-config-trigger:hover:not(:disabled) { + background: hsl(44 88% 84%); + color: hsl(35 74% 25%); +} +.cw-ab-remove:hover { + background: hsl(var(--secondary) / 0.62); + color: hsl(var(--foreground)); +} +.cw-ab-config-trigger:disabled, +.cw-ab-remove:disabled { + cursor: default; + opacity: 0.45; +} +.cw-ab-remove { + width: 28px; + padding: 0; + background: transparent; +} +.cw-ab-config-head { + min-height: 68px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px 10px; + background: hsl(var(--background) / 0.94); +} +.cw-ab-config-head > div { + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.cw-ab-config-head strong { + font-size: 16px; + font-weight: 680; +} +.cw-ab-config-head span { + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.cw-ab-config-done { + min-height: 32px; + padding: 0 11px; + border: 0; + border-radius: 8px; + background: hsl(var(--foreground)); + color: hsl(var(--background)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 650; +} +.cw-ab-config-done-wrap { + position: relative; + flex: 0 0 auto; + display: inline-flex; + border-radius: 8px; +} +.cw-ab-config-done:disabled { + background: hsl(var(--secondary) / 0.82); + color: hsl(var(--muted-foreground) / 0.68); + cursor: not-allowed; +} +.cw-ab-config-done-tip { + position: absolute; + z-index: 8; + right: 0; + bottom: calc(100% + 7px); + width: max-content; + max-width: 190px; + padding: 6px 8px; + border-radius: 7px; + background: hsl(var(--foreground)); + color: hsl(var(--background)); + font-size: 11px; + font-weight: 400; + line-height: 1.4; + opacity: 0; + pointer-events: none; + transform: translateY(3px); + transition: opacity 140ms ease, transform 140ms ease; +} +.cw-ab-config-done-wrap.is-disabled:hover .cw-ab-config-done-tip, +.cw-ab-config-done-wrap.is-disabled:focus-visible .cw-ab-config-done-tip { + opacity: 1; + transform: translateY(0); +} +.cw-ab-config-done-wrap:focus-visible { + outline: 2px solid hsl(var(--foreground) / 0.18); + outline-offset: 2px; +} +.cw-ab-config { + flex: 0 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + gap: 12px; + padding: 12px 16px 16px; + background: hsl(var(--secondary) / 0.16); +} +.cw-ab-config > label, +.cw-ab-config fieldset { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; + margin: 0; + padding: 0; + border: 0; +} +.cw-ab-config > label > span, +.cw-ab-config legend { + color: hsl(var(--muted-foreground)); + font-size: 13px; + font-weight: 650; +} +.cw-ab-config legend { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.cw-ab-config legend em { + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + font-style: normal; + font-weight: 550; +} +.cw-ab-config input[type="text"], +.cw-ab-config > label > input, +.cw-ab-config > label > textarea { + width: 100%; + min-height: 40px; + padding: 8px 11px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 13px; +} +.cw-ab-config > label > textarea { + min-height: 58px; + max-height: 132px; + resize: vertical; + line-height: 1.55; +} +.cw-ab-optimization-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} +.cw-ab-optimization-list label { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 9px; + border-radius: 8px; + background: hsl(var(--background) / 0.82); + color: hsl(var(--foreground)); + font-size: 12.5px; + cursor: not-allowed; + opacity: 0.5; +} +.cw-ab-optimization-list input { + width: 15px; + height: 15px; + margin: 0; + accent-color: hsl(var(--foreground)); +} +.cw-ab-config > p { + margin: 0; + color: hsl(var(--muted-foreground)); + font-size: 12.5px; + line-height: 1.5; +} +.cw-ab-conversation { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 14px; +} +.cw-ab-empty { + height: 100%; + min-height: 210px; + display: grid; + place-items: center; + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.cw-ab-launch { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 9px; + text-align: center; +} +.cw-ab-launch-hint { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-ab-starting { + align-content: center; + gap: 8px; +} +.cw-ab-starting .cw-i { + width: 18px; + height: 18px; +} +.cw-ab-start { + min-width: 118px; + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 0 13px; + border: 0; + border-radius: 9px; + background: hsl(var(--secondary) / 0.72); + color: hsl(var(--foreground)); + cursor: pointer; + font: inherit; + font-size: 11.5px; + font-weight: 580; + transition: background-color 160ms ease, box-shadow 160ms ease; +} +.cw-ab-start:hover:not(:disabled) { + background: hsl(var(--secondary)); + box-shadow: none; +} +.cw-ab-start:disabled { + background: hsl(var(--secondary) / 0.42); + color: hsl(var(--muted-foreground) / 0.62); + cursor: not-allowed; +} +.cw-ab-start .cw-i { + width: 15px; + height: 15px; +} +.cw-ab-deploy-footer { + flex: 0 0 auto; + display: flex; + justify-content: flex-end; + padding: 0 12px 12px; +} +.cw-ab-deploy { + min-height: 32px; + padding: 0 13px; + border: 0; + border-radius: 8px; + background: #111; + color: #fff; + cursor: pointer; + font: inherit; + font-size: 11.5px; + font-weight: 620; + transition: background-color 160ms ease, box-shadow 160ms ease; +} +.cw-ab-deploy:hover:not(:disabled) { + background: #29292b; + box-shadow: 0 6px 16px hsl(0 0% 0% / 0.14); +} +.cw-ab-deploy:disabled { + cursor: not-allowed; + opacity: 0.42; +} +.cw-ab-add { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 7px; + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease; +} +.cw-ab-add:hover { + border-color: hsl(var(--foreground) / 0.38); + background: hsl(var(--secondary) / 0.24); + color: hsl(var(--foreground)); +} +.cw-ab-add .cw-i { + width: 22px; + height: 22px; +} +.cw-ab-add strong { + font-size: 13px; +} +.cw-ab-add span { + font-size: 10.5px; +} +.cw-ab-composer { + position: relative; + z-index: 2; + min-width: 0; + padding: 0 var(--cw-workspace-gutter) 18px; + background: #fff; +} +.cw-ab-composer .cw-debug-composerbox { + width: min(100%, 860px); + min-height: 48px; + margin: 0 auto; + border-color: hsl(var(--foreground) / 0.14); + border-radius: 14px; + background: hsl(var(--background) / 0.92); + box-shadow: 0 12px 32px hsl(var(--foreground) / 0.06); + backdrop-filter: blur(12px); +} +.cw-debug.is-standalone { + flex: 1; + width: 100%; + min-width: 0; + border-left: 0; + background: transparent; +} +.cw-debug.is-standalone .cw-debug-head { + height: 58px; + padding-inline: 22px; + background: hsl(var(--panel) / 0.7); +} +.cw-debug.is-standalone .cw-debug-title { + font-size: 15px; +} +.cw-debug.is-standalone .cw-debug-body { + padding: 22px clamp(18px, 5vw, 72px) 28px; +} +.cw-debug.is-standalone .cw-debug-chat { + width: min(100%, 840px); + margin: 0 auto; +} +.cw-debug.is-standalone .cw-debug-chat-empty { + min-height: 260px; + border: 1px dashed hsl(var(--border)); + border-radius: 16px; + background: hsl(var(--panel) / 0.56); +} +.cw-debug.is-standalone .cw-debug-composer { + padding: 12px 210px 20px clamp(18px, 5vw, 72px); + background: transparent; +} +.cw-debug.is-standalone .cw-debug-composerbox { + width: min(100%, 840px); + min-height: 48px; + margin: 0 auto; + border-color: hsl(var(--foreground) / 0.14); + border-radius: 14px; + box-shadow: 0 12px 32px hsl(var(--foreground) / 0.06); +} +.cw-debug.is-standalone .cw-debug-overlay { + background: hsl(var(--background) / 0.68); +} +.cw-debug.is-standalone .cw-debug-overlay-content { + width: min(100%, 390px); + padding: 28px; + border-radius: 16px; +} +.cw-validation-prototype { + flex: 1; + min-width: 0; + min-height: 0; + overflow-y: auto; + padding: clamp(26px, 4vw, 56px); +} +.cw-validation-page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + margin: 0 auto 28px; + max-width: 1040px; +} +.cw-eyebrow { + color: hsl(var(--cw-workspace-accent)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.15em; +} +.cw-validation-page-head h2 { + margin: 5px 0 4px; + color: hsl(var(--cw-workspace-ink)); + font-size: clamp(24px, 3vw, 34px); + font-weight: 720; + letter-spacing: -0.045em; +} +.cw-validation-page-head p { + margin: 0; + color: hsl(var(--muted-foreground)); + font-size: 13px; +} +.cw-prototype-action { + min-height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 0 14px; + border: 1px solid hsl(var(--cw-workspace-ink)); + border-radius: 8px; + background: hsl(var(--cw-workspace-ink)); + color: hsl(var(--background)); + font: inherit; + font-size: 12px; + font-weight: 650; +} +.cw-prototype-action:disabled { + cursor: not-allowed; + opacity: 0.72; +} +.cw-dataset-summary, +.cw-variant-grid, +.cw-metric-board, +.cw-prototype-table, +.cw-run-list, +.cw-prototype-note { + width: min(100%, 1040px); + margin-inline: auto; +} +.cw-dataset-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-bottom: 14px; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); + box-shadow: 0 10px 32px hsl(var(--foreground) / 0.035); +} +.cw-dataset-summary > div { + display: flex; + flex-direction: column; + gap: 4px; + padding: 17px 20px; +} +.cw-dataset-summary > div + div { + border-left: 1px solid hsl(var(--border)); +} +.cw-dataset-summary strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 22px; + letter-spacing: -0.04em; +} +.cw-dataset-summary span { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-prototype-table { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); +} +.cw-prototype-row { + min-height: 54px; + display: grid; + grid-template-columns: minmax(180px, 1.3fr) minmax(100px, 0.7fr) minmax(170px, 1fr) 82px; + align-items: center; + gap: 16px; + padding: 10px 16px; + border-top: 1px solid hsl(var(--border)); + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.cw-prototype-row.is-head { + min-height: 38px; + border-top: 0; + background: hsl(var(--secondary) / 0.42); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.cw-prototype-row > strong { + color: hsl(var(--foreground)); + font-size: 12px; + font-weight: 600; +} +.cw-status-pill, +.cw-run-status, +.cw-run-kind { + justify-self: start; + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 600; +} +.cw-variant-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 14px; +} +.cw-variant-card { + position: relative; + overflow: hidden; + padding: 20px; + border: 1px solid hsl(var(--border)); + border-radius: 14px; + background: hsl(var(--panel)); + box-shadow: 0 12px 34px hsl(var(--foreground) / 0.04); +} +.cw-variant-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: hsl(var(--foreground) / 0.22); +} +.cw-variant-card.is-candidate::before { + background: hsl(var(--cw-workspace-accent)); +} +.cw-variant-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 28px; + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.cw-variant-head small { + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + font-size: 9.5px; + letter-spacing: 0; + text-transform: none; +} +.cw-variant-card > strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 17px; + letter-spacing: -0.025em; +} +.cw-variant-card > p { + min-height: 42px; + margin: 7px 0 22px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.6; +} +.cw-variant-card dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin: 0; +} +.cw-variant-card dl > div { + padding: 9px 10px; + border-radius: 8px; + background: hsl(var(--secondary) / 0.5); +} +.cw-variant-card dt { + color: hsl(var(--muted-foreground)); + font-size: 9.5px; +} +.cw-variant-card dd { + margin: 3px 0 0; + color: hsl(var(--foreground)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 10.5px; +} +.cw-metric-board { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); +} +.cw-metric-head, +.cw-metric-row { + display: grid; + grid-template-columns: minmax(170px, 1fr) 100px 100px 88px; + align-items: center; + gap: 12px; + padding: 11px 16px; +} +.cw-metric-head { + grid-template-columns: auto auto minmax(0, 1fr); + min-height: 48px; + border-bottom: 1px solid hsl(var(--border)); +} +.cw-metric-head .cw-i { + width: 15px; + color: hsl(var(--cw-workspace-accent)); +} +.cw-metric-head strong { + font-size: 12px; +} +.cw-metric-head span { + justify-self: end; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-metric-row { + min-height: 44px; + color: hsl(var(--muted-foreground)); + font-size: 11px; +} +.cw-metric-row + .cw-metric-row { + border-top: 1px solid hsl(var(--border) / 0.7); } -.cw-debug-composer { - flex-shrink: 0; - padding: 10px 14px 14px; - background: hsl(var(--panel)); +.cw-metric-row strong { + color: hsl(var(--foreground)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; } -.cw-debug-composerbox { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 6px 6px 10px; +.cw-metric-row em { + color: hsl(var(--cw-workspace-accent)); + font-size: 10.5px; + font-style: normal; + font-weight: 650; +} +.cw-run-list { + overflow: hidden; border: 1px solid hsl(var(--border)); - border-radius: 24px; - background: hsl(var(--background)); + border-radius: 12px; + background: hsl(var(--panel)); } -.cw-debug-input { - flex: 1; - min-width: 0; - max-height: 120px; - padding: 8px 4px; - border: none; - outline: none; - resize: none; - overflow-y: auto; - background: transparent; - color: hsl(var(--foreground)); - font: inherit; - font-size: 14px; - line-height: 1.5; +.cw-run-row { + min-height: 72px; + display: grid; + grid-template-columns: 36px minmax(220px, 1fr) 70px 110px 72px; + align-items: center; + gap: 12px; + padding: 11px 16px; } -.cw-debug-input::placeholder { - color: hsl(var(--muted-foreground)); +.cw-run-row + .cw-run-row { + border-top: 1px solid hsl(var(--border)); } -.cw-debug-send { - flex-shrink: 0; +.cw-run-icon { + width: 32px; + height: 32px; display: inline-flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - cursor: pointer; - transition: - opacity 0.15s, - transform 0.1s, - background 0.12s, - color 0.12s; + border-radius: 9px; + background: hsl(var(--cw-workspace-accent-soft)); + color: hsl(var(--cw-workspace-accent)); } -.cw-debug-send { - background: hsl(var(--primary)); - color: hsl(var(--primary-foreground)); +.cw-run-icon .cw-i { + width: 14px; + height: 14px; } -.cw-debug-send:hover:not(:disabled) { - opacity: 0.85; +.cw-run-row > div { + min-width: 0; } -.cw-debug-send:active:not(:disabled) { - transform: scale(0.94); +.cw-run-row strong { + color: hsl(var(--foreground)); + font-size: 12px; } -.cw-debug-send:disabled { - opacity: 0.3; - cursor: default; +.cw-run-row p { + margin: 3px 0 0; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + text-overflow: ellipsis; + white-space: nowrap; } -.cw-debug-overlay { - position: absolute; - z-index: 5; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 22px; - background: hsl(var(--panel) / 0.5); - backdrop-filter: blur(9px) saturate(0.9); - -webkit-backdrop-filter: blur(9px) saturate(0.9); +.cw-run-row time { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; } -.cw-debug-overlay-content { - width: min(100%, 290px); +.cw-run-status.is-running { + background: hsl(var(--cw-workspace-accent-soft)); + color: hsl(var(--cw-workspace-accent)); +} +.cw-prototype-note { display: flex; - flex-direction: column; align-items: center; - gap: 10px; - padding: 18px; - border: 1px solid hsl(var(--border) / 0.72); - border-radius: 12px; - background: hsl(var(--background) / 0.82); - box-shadow: 0 10px 30px hsl(var(--foreground) / 0.08); - text-align: center; -} -.cw-debug-overlay-title { - color: hsl(var(--foreground)); - font-size: 14px; - font-weight: 650; -} -.cw-debug-overlay-copy { + gap: 7px; + margin-top: 14px; color: hsl(var(--muted-foreground)); - font-size: 12.5px; - line-height: 1.55; + font-size: 10.5px; } -.cw-debug-overlay-progress { - width: 100%; - display: flex; - flex-direction: column; - gap: 8px; +.cw-prototype-note .cw-i { + width: 13px; + height: 13px; } -.cw-debug-overlay-actions { +.cw-publish-loading { + flex: 1; display: flex; + flex-direction: column; align-items: center; justify-content: center; - gap: 8px; - margin-top: 2px; -} -.cw-debug-ignore { - min-height: 30px; - padding: 6px 11px; - border: 1px solid hsl(var(--border)); - border-radius: 8px; - background: hsl(var(--background) / 0.72); - color: hsl(var(--foreground)); - font: inherit; + gap: 7px; + color: hsl(var(--muted-foreground)); font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: - background 0.12s, - border-color 0.12s, - transform 0.1s; -} -.cw-debug-ignore:hover:not(:disabled) { - border-color: hsl(var(--foreground) / 0.2); - background: hsl(var(--background)); } -.cw-debug-ignore:active:not(:disabled) { - transform: scale(0.96); +.cw-publish-loading .cw-i { + width: 22px; + height: 22px; + margin-bottom: 5px; + color: hsl(var(--cw-workspace-accent)); } -.cw-debug-ignore:disabled { - opacity: 0.45; - cursor: default; +.cw-publish-loading strong { + color: hsl(var(--foreground)); + font-size: 14px; } /* ---------- header ---------- */ @@ -1991,70 +3109,65 @@ flex-direction: column; } -/* ---------- agent type segmented control ---------- */ -.cw-typeradio { - display: flex; -} -.cw-typeradio--row { - position: relative; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - gap: var(--cw-agent-type-gap); - height: 44px; - width: 100%; - max-width: 100%; - padding: 4px; - overflow: visible; - border: 1px solid hsl(var(--border) / 0.55); - border-radius: 10px; - background: hsl(var(--secondary) / 0.58); -} -.cw-typeradio-slider { - position: absolute; - z-index: 0; - top: 4px; - bottom: 4px; - left: 4px; - width: var(--cw-agent-type-slider-width); - border: 1px solid hsl(var(--border) / 0.72); - border-radius: 7px; - background: hsl(var(--background)); - transform: translateX(var(--cw-active-type-offset)); - transition: transform 0.24s cubic-bezier(0.22, 1, 0.36, 1); +/* ---------- agent type radios ---------- */ +.cw-agent-type-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 8px; } -.cw-typeradio-item { +.cw-agent-type-option { position: relative; - z-index: 1; - flex: 1 1 0; min-width: 0; - min-height: 34px; + min-height: 58px; display: flex; align-items: center; - justify-content: center; - padding: 7px 10px; - border: none; - border-radius: 7px; - background: transparent; - color: hsl(var(--muted-foreground)); + gap: 10px; + padding: 10px 12px; + border: 1px solid hsl(var(--border) / 0.72); + border-radius: 10px; + background: #fff; + color: hsl(var(--foreground)); cursor: pointer; - transition: - color 0.16s, - background 0.16s; + transition: border-color 150ms ease, background-color 150ms ease; } -.cw-typeradio-item:hover { - background: hsl(var(--foreground) / 0.035); - color: hsl(var(--foreground)); +.cw-agent-type-option:hover { + border-color: hsl(var(--foreground) / 0.2); + background: hsl(var(--secondary) / 0.28); +} +.cw-agent-type-option.is-on { + border-color: hsl(var(--foreground) / 0.3); + background: hsl(var(--secondary) / 0.42); } -.cw-typeradio-item.is-disabled { - color: hsl(var(--muted-foreground) / 0.45); +.cw-agent-type-option.is-disabled { + color: hsl(var(--muted-foreground) / 0.52); cursor: not-allowed; } -.cw-typeradio-item.is-disabled:hover { - background: transparent; - color: hsl(var(--muted-foreground) / 0.45); +.cw-agent-type-radio { + width: 15px; + height: 15px; + flex: 0 0 15px; + margin: 0; + accent-color: hsl(var(--foreground)); +} +.cw-agent-type-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.cw-agent-type-copy strong { + font-size: 13px; + font-weight: 650; } -.cw-typeradio-disabled-hint { +.cw-agent-type-copy small { + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-agent-type-disabled-hint { position: absolute; top: calc(100% + 17px); right: 0; @@ -2078,35 +3191,15 @@ opacity 0.14s ease, transform 0.14s ease; } -.cw-typeradio-item.is-disabled:hover .cw-typeradio-disabled-hint, -.cw-typeradio-item.is-disabled:focus .cw-typeradio-disabled-hint, -.cw-typeradio-item.is-disabled:focus-visible .cw-typeradio-disabled-hint { +.cw-agent-type-option.is-disabled:hover .cw-agent-type-disabled-hint, +.cw-agent-type-option.is-disabled:focus .cw-agent-type-disabled-hint, +.cw-agent-type-option.is-disabled:focus-visible .cw-agent-type-disabled-hint { opacity: 1; transform: translateY(0); } -.cw-typeradio-item.is-on { - background: transparent; - color: hsl(var(--foreground)); -} -.cw-typeradio-item:has(.cw-typeradio-input:focus-visible) { +.cw-agent-type-option:has(.cw-agent-type-radio:focus-visible) { box-shadow: inset 0 0 0 2px hsl(var(--ring) / 0.45); } -/* Visually-hidden native input keeps keyboard/a11y behavior. */ -.cw-typeradio-input { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} -.cw-typeradio-title { - min-width: 0; - white-space: normal; - word-break: keep-all; - font-size: clamp(11px, 1.1vw, 13px); - font-weight: 600; - line-height: 1.12; - text-align: center; -} .cw-add-sub { display: inline-flex; align-items: center; @@ -2797,6 +3890,11 @@ /* ---------- responsive ---------- */ @media (max-width: 1280px) { + .cw-workspace-header { + grid-template-columns: minmax(160px, 1fr) auto minmax(160px, 1fr); + gap: 16px; + padding-inline: 16px; + } .cw-debug { width: 280px; } @@ -2806,16 +3904,22 @@ } @media (max-width: 1080px) { + .cw-workspace-header { + grid-template-columns: minmax(140px, 1fr) auto minmax(140px, 1fr); + } .cw-editor { - flex-wrap: wrap; - overflow-y: auto; + flex-wrap: nowrap; + overflow: hidden; } .cw-tree { - height: 260px; + height: auto; } .cw-detail { - height: min(720px, calc(100dvh - 120px)); - min-height: 560px; + flex-basis: 58%; + width: auto; + max-width: 560px; + height: auto; + min-height: 0; } .cw-debug { flex: 0 0 100%; @@ -2850,12 +3954,49 @@ } @media (max-width: 860px) { + .cw-root { + --cw-workspace-gutter: 8px; + } + .cw-workspace-header { + min-height: 108px; + grid-template-columns: minmax(0, 1fr); + gap: 8px; + margin: 8px var(--cw-workspace-gutter) 0; + padding: 9px 10px; + } + .cw-workspace-stepper { + grid-column: 1; + width: min(100%, 340px); + justify-self: center; + } + .cw-workspace-actions { + position: absolute; + top: 10px; + right: 12px; + grid-column: 1; + } + .cw-validation-workspace { + display: flex; + } + .cw-ab-stage { + padding: 8px var(--cw-workspace-gutter); + } + .cw-ab-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .cw-ab-composer { + padding-inline: var(--cw-workspace-gutter); + } .cw-editor { flex-direction: column; flex-wrap: nowrap; overflow-x: hidden; overflow-y: auto; } + .cw-editor > .abc-root { + width: 100%; + min-width: 0; + } .cw-tree { width: 100%; height: auto; @@ -2869,11 +4010,11 @@ height: min(720px, calc(100dvh - 120px)); min-height: 560px; } - .cw-typebar { - padding-inline: 12px; - } .cw-detail-scroll { - padding: 20px 12px 40px; + padding: 20px 12px 90px; + } + .cw-build-next { + bottom: 14px; } .cw-debug { flex: none; @@ -2896,10 +4037,23 @@ } @media (max-width: 700px) { - .cw-typeradio-item { - padding-inline: 6px; + .cw-workspace-stepper button { + padding-inline: 4px; } - .cw-typeradio-title { - font-size: 12.5px; + .cw-optimization-list { + grid-template-columns: minmax(0, 1fr); + } + .cw-ab-grid { + grid-template-columns: minmax(0, 1fr); + } + .cw-ab-config { + grid-template-columns: minmax(0, 1fr); + } + .cw-ab-composer { + padding-bottom: 12px; + } + .cw-dataset-summary > div + div { + border-top: 1px solid hsl(var(--border)); + border-left: 0; } } diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index 10058ca4..1eb1fa41 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -10,7 +10,6 @@ import { } from "react"; import { AnimatePresence, motion } from "motion/react"; import { - ArrowRight, ArrowUp, Bot, Boxes, @@ -69,6 +68,7 @@ import { displayDescription } from "./displayText"; import { localPickerMatches } from "./localPickerSearch"; import { draftToYaml } from "./configYaml"; import type { AgentProject } from "./project"; +import { AgentBuildCanvas } from "./AgentBuildCanvas"; import type { SkillSource } from "./skills/types"; import { SkillHubPicker } from "./SkillHubPicker"; import { LocalPicker } from "./LocalPicker"; @@ -83,6 +83,7 @@ import { } from "./vikingKnowledgebases"; import { ProjectPreview, + type DeployResult, type DeploymentTaskUpdate, } from "../ui/ProjectPreview"; import { Blocks, ThinkingPlaceholder } from "../ui/Blocks"; @@ -146,7 +147,7 @@ interface StepMeta { const STEPS: StepMeta[] = [ { id: "type", - label: "类型", + label: "Agent 类型", hint: "选择 Agent 类型", icon: Shapes, required: true, @@ -188,35 +189,6 @@ function ClearAgentIcon({ className }: { className?: string }) { ); } -/** Custom debug-console mark: a compact runtime panel with a live trace. */ -function DebugConsoleIcon({ className }: { className?: string }) { - return ( - - ); -} - /** Debug-run mark: a play head breaking through two lightweight motion rails. */ function DebugRunIcon({ className }: { className?: string }) { return ( @@ -275,18 +247,28 @@ function A2aRefreshIcon({ className }: { className?: string }) { ); } -const AGENT_TYPE_GAP_PX = 4; const AGENT_TYPE_BAR_LABELS: Record< NonNullable, string > = { - llm: "LLM 智能体", - sequential: "顺序型智能体", - parallel: "并行型智能体", - loop: "循环型智能体", + llm: "智能体", + sequential: "分步协作", + parallel: "同时处理", + loop: "循环执行", a2a: "远程智能体", }; +const AGENT_TYPE_DESCRIPTIONS: Record< + NonNullable, + string +> = { + llm: "理解任务并完成一个具体工作", + sequential: "内部步骤按照顺序依次执行", + parallel: "内部步骤同时工作,完成后统一汇总", + loop: "重复执行内部步骤,直到满足停止条件", + a2a: "调用已经存在的远程 Agent", +}; + const A2A_REGISTRY_ENV_TO_FIELD = { REGISTRY_SPACE_ID: "registrySpaceId", REGISTRY_TOP_K: "registryTopK", @@ -1336,6 +1318,18 @@ function addChild(root: AgentDraft, path: NodePath): AgentDraft { })); } +function insertChild( + root: AgentDraft, + parentPath: NodePath, + index: number, +): AgentDraft { + return updateNode(root, parentPath, (n) => { + const subAgents = n.subAgents.slice(); + subAgents.splice(index, 0, emptyDraft()); + return { ...n, subAgents }; + }); +} + function removeNode(root: AgentDraft, path: NodePath): AgentDraft { if (path.length === 0) return root; // the root is never removable const parentPath = path.slice(0, -1); @@ -1500,7 +1494,7 @@ function collectDeploymentEnv(root: AgentDraft): RuntimeEnvConfiguration { /* ---------------------------------------------------------------- * * Left structure tree: one selectable, editable node (recursive). * ---------------------------------------------------------------- */ -function TreeNode({ +export function TreeNode({ root, path, selectedPath, @@ -1681,9 +1675,9 @@ function TreeNode({ ); } -type DebugPhase = - "idle" | "building" | "starting" | "ready" | "sending" | "error"; +type DebugPhase = "idle" | "starting" | "ready" | "sending" | "error"; +type WorkspaceMode = "build" | "validate" | "publish"; interface DebugMessage { role: "user" | "assistant"; content: string; @@ -1691,6 +1685,20 @@ interface DebugMessage { error?: string; } +interface DebugVariant { + id: string; + name: string; + modelName: string; + description: string; + instruction: string; + optimizations: string[]; + configOpen: boolean; + phase: DebugPhase; + runtimeSnapshot: string; + messages: DebugMessage[]; + error: string | null; +} + function codegenDraft(draft: AgentDraft): AgentDraft { return { ...draft, @@ -1700,6 +1708,13 @@ function codegenDraft(draft: AgentDraft): AgentDraft { }; } +function workspaceAgentName(draft: AgentDraft): string { + const rootName = draft.name.trim(); + if (rootName) return rootName; + if (draft.agentType !== "sequential") return ""; + return draft.subAgents.find((agent) => agent.name.trim())?.name.trim() ?? ""; +} + function debugRuntimeDraft(draft: AgentDraft): AgentDraft { const runtimeEnv = collectDeploymentEnv(draft); const values = { @@ -1724,188 +1739,386 @@ function debugSnapshotKey(draft: AgentDraft): string { return JSON.stringify(debugRuntimeDraft(draft)); } -function DebugPanel({ +function debugVariantSnapshot( + draftSnapshot: string, + variant: Pick< + DebugVariant, + "modelName" | "description" | "instruction" | "optimizations" + >, +): string { + return JSON.stringify({ + draftSnapshot, + modelName: variant.modelName, + description: variant.description, + instruction: variant.instruction, + optimizations: variant.optimizations, + }); +} + +function debugVariantConfigurationKey( + variant: Pick< + DebugVariant, + "modelName" | "description" | "instruction" | "optimizations" + >, +): string { + return JSON.stringify({ + modelName: variant.modelName.trim(), + description: variant.description.trim(), + instruction: variant.instruction.trim(), + optimizations: variant.optimizations, + }); +} + +function DebugComparisonWorkspace({ enabled, disabledReason, - phase, - stale, - run, - projectName, - logs, - messages, + variants, + draftSnapshot, input, - error, - deploying, - deployError, onInput, onSend, - onRestart, - onIgnoreChanges, - onDeploy, + onStartVariant, + onDeployVariant, + onAddVariant, + onRemoveVariant, + onToggleConfig, + onCompleteConfig, + onConfigChange, }: { enabled: boolean; disabledReason: string; - phase: DebugPhase; - stale: boolean; - run: GeneratedAgentTestRun | null; - projectName: string; - logs: string[]; - messages: DebugMessage[]; + variants: DebugVariant[]; + draftSnapshot: string; input: string; - error: string | null; - deploying: boolean; - deployError: string; onInput: (v: string) => void; onSend: () => void; - onRestart: () => void; - onIgnoreChanges: () => void; - onDeploy: () => void; + onStartVariant: (id: string) => void; + onDeployVariant: (id: string) => void; + onAddVariant: () => void; + onRemoveVariant: (id: string) => void; + onToggleConfig: (id: string) => void; + onCompleteConfig: (id: string) => void; + onConfigChange: ( + id: string, + field: "modelName" | "description" | "instruction", + value: string, + ) => void; }) { - const [collapsed, setCollapsed] = useState(false); - const ready = phase === "ready" || phase === "sending"; - const busy = - phase === "building" || phase === "starting" || phase === "sending"; - const showInitialOverlay = enabled && !run && phase === "idle"; - const showProgressOverlay = - enabled && (phase === "building" || phase === "starting"); - const showStaleOverlay = Boolean(run && stale && !showProgressOverlay); - - if (collapsed) { + const runningVariants = variants.filter((variant) => { + if (variant.phase !== "ready") return false; return ( - + variant.runtimeSnapshot === debugVariantSnapshot(draftSnapshot, variant) ); - } + }); + const sending = variants.some((variant) => variant.phase === "sending"); + const canSend = runningVariants.length > 0 && !sending; return ( -