From 66ec9d9cc7b5480fdd27e4774f58cbe06d15fe7f Mon Sep 17 00:00:00 2001 From: sam2tom Date: Wed, 16 Sep 2026 13:41:17 +0800 Subject: [PATCH 1/3] feat(web): align Agents UI and Environment overview --- apps/web/e2e/agents-lifecycle.spec.ts | 100 ++- apps/web/src/App.tsx | 118 +-- apps/web/src/components/CreateMenu.test.tsx | 24 + apps/web/src/components/CreateMenu.tsx | 132 +++ .../src/components/ProductNavigation.test.tsx | 18 + apps/web/src/components/ProductNavigation.tsx | 31 + apps/web/src/features/agents/AgentForm.tsx | 113 ++- .../features/agents/AgentSetupView.test.tsx | 50 ++ .../src/features/agents/AgentSetupView.tsx | 145 ++++ apps/web/src/features/agents/AgentsView.tsx | 80 +- .../src/features/agents/agent-form.test.ts | 22 +- apps/web/src/features/agents/agent-form.ts | 56 +- .../src/features/agents/agent-preview.test.ts | 46 ++ apps/web/src/features/agents/agent-preview.ts | 76 ++ .../environments/EnvironmentsView.test.tsx | 112 +++ .../environments/EnvironmentsView.tsx | 171 ++++ .../src/features/sessions/SessionsView.tsx | 12 + apps/web/src/style.css | 766 ++++++++++++++++++ docs/protocol-coverage.md | 21 +- 19 files changed, 1989 insertions(+), 104 deletions(-) create mode 100644 apps/web/src/components/CreateMenu.test.tsx create mode 100644 apps/web/src/components/CreateMenu.tsx create mode 100644 apps/web/src/components/ProductNavigation.test.tsx create mode 100644 apps/web/src/components/ProductNavigation.tsx create mode 100644 apps/web/src/features/agents/AgentSetupView.test.tsx create mode 100644 apps/web/src/features/agents/AgentSetupView.tsx create mode 100644 apps/web/src/features/agents/agent-preview.test.ts create mode 100644 apps/web/src/features/agents/agent-preview.ts create mode 100644 apps/web/src/features/environments/EnvironmentsView.test.tsx create mode 100644 apps/web/src/features/environments/EnvironmentsView.tsx diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index e991d14..43316db 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -154,6 +154,8 @@ test("retrieves latest details and reuses a validated create/edit form", async ( await name.fill(""); await page.getByLabel("Instructions").fill(""); await page.getByLabel("Metadata").fill('{"team":"acceptance"}'); + await page.getByLabel("Reasoning effort").selectOption(""); + await page.getByLabel("Reasoning summary").selectOption(""); await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByRole("button", { name: "Edit" })).toBeFocused(); @@ -164,34 +166,57 @@ test("retrieves latest details and reuses a validated create/edit form", async ( name: null, instructions: null, metadata: { team: "acceptance" }, + reasoning: { effort: null, summary: null }, }); expect(browserErrors).toEqual([]); }); -test("supports keyboard creation, traps focus, and returns focus on Escape", async ({ page, request }) => { +test("supports global Create keyboard navigation and consumes setup requests once", async ({ page, request }) => { await openAgents(page, request); - const trigger = page.getByRole("button", { name: "New Agent" }); - await trigger.click(); + const createMenu = page.getByRole("button", { name: "Create", exact: true }); + await createMenu.focus(); + await page.keyboard.press("Enter"); + await expect(page.getByRole("menuitem", { name: /^Agent\b/ })).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(createMenu).toBeFocused(); + await page.keyboard.press("ArrowDown"); + const createAgentItem = page.getByRole("menuitem", { name: /^Agent\b/ }); + const startSessionItem = page.getByRole("menuitem", { name: /^Start Session\b/ }); + await expect(createAgentItem).toBeFocused(); + await page.keyboard.press("ArrowDown"); + await expect(startSessionItem).toBeFocused(); + await page.keyboard.press("ArrowUp"); + await expect(createAgentItem).toBeFocused(); + await page.keyboard.press("Enter"); await expect(page.getByLabel("Name")).toBeFocused(); + await expect(page.getByRole("heading", { name: "Request preview" })).toBeVisible(); - const close = page.getByRole("button", { name: "Close dialog" }); - await close.focus(); - await page.keyboard.press("Shift+Tab"); - await expect(page.getByRole("button", { name: "Create Agent" })).toBeFocused(); + await page.getByRole("button", { name: "Sessions", exact: true }).click(); + await page.getByRole("button", { name: "Agents", exact: true }).click(); + await expect(page.getByRole("table", { name: "Agents" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "New Agent" })).toHaveCount(0); - await page.getByLabel("Name").focus(); - await page.keyboard.press("Escape"); - await expect(page.getByRole("dialog")).toHaveCount(0); - await expect(trigger).toBeFocused(); const detailTrigger = page.getByRole("button", { name: /Open details for Lifecycle Agent/ }); await detailTrigger.focus(); await detailTrigger.evaluate((button) => button.click()); await expect(page.getByRole("dialog").getByRole("heading", { name: "Lifecycle Agent", exact: true })).toBeVisible(); await page.keyboard.press("Escape"); + await expect(page.getByRole("dialog")).toHaveCount(0); await expect(detailTrigger).toBeFocused(); await expect(page.locator(".modal-backdrop")).toHaveCount(0); - await trigger.click(); + await createMenu.click(); + await startSessionItem.click(); + await expect(page.getByRole("dialog", { name: "Start an idle Session" })).toBeVisible(); + await page.getByRole("dialog", { name: "Start an idle Session" }).getByRole("button", { name: "Cancel" }).click(); + await expect(createMenu).toBeFocused(); + await page.getByRole("button", { name: "Agents", exact: true }).click(); + await page.getByRole("button", { name: "Sessions", exact: true }).click(); + await expect(page.getByRole("dialog", { name: "Start an idle Session" })).toHaveCount(0); + + await page.getByRole("button", { name: "Agents", exact: true }).click(); + await createMenu.click(); + await createAgentItem.click(); const createMetadata = page.locator(".agent-metadata-input"); const createName = page.locator('input[data-agent-initial-focus="true"]'); await expect(createName).toBeFocused(); @@ -200,7 +225,20 @@ test("supports keyboard creation, traps focus, and returns focus on Escape", asy await expect(createName).toHaveValue(""); await createName.focus(); await page.keyboard.press("Enter"); - await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByRole("status")).toContainText("Agent definition saved as"); + + await createMenu.click(); + await createAgentItem.click(); + await expect(page.getByLabel("Name")).toHaveValue(""); + await expect(page.getByLabel("Name")).toBeEnabled(); + await expect(page.getByRole("status")).toHaveCount(0); + await page.getByRole("button", { name: "Back to Agents" }).click(); + await expect(createMenu).toBeFocused(); + + const ledgerCreate = page.getByRole("button", { name: "New Agent" }); + await ledgerCreate.click(); + await page.getByRole("button", { name: "Back to Agents" }).click(); + await expect(ledgerCreate).toBeFocused(); const requests = await fixtureRequests(request); const creates = requests.filter((entry) => entry.method === "POST" && entry.path === "/v1/agents"); @@ -317,17 +355,37 @@ test("keeps the Agent ledger and dialogs usable at 390 px in light and dark mode expect(metrics.ledger?.right).toBeLessThanOrEqual(390); await attachScreenshot(page, testInfo, "narrow-light-agent-ledger"); - await page.getByRole("button", { name: "New Agent" }).click(); - const dialog = page.getByRole("dialog"); - const box = await dialog.boundingBox(); + await page.getByRole("button", { name: "Environments", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Environments Observed" })).toBeVisible(); + const sessionsNavigation = page.getByRole("button", { name: "Sessions", exact: true }); + await sessionsNavigation.click(); + await expect(sessionsNavigation).toHaveAttribute("aria-current", "page"); + await expect(page.locator(".session-page")).toBeVisible(); + await page.getByRole("button", { name: "Agents", exact: true }).click(); + + const globalCreate = page.getByRole("button", { name: "Create", exact: true }); + await globalCreate.click(); + const createPanel = page.getByRole("menu", { name: "Create" }); + await expect(createPanel).toBeVisible(); + const createPanelBox = await createPanel.boundingBox(); + expect(createPanelBox).not.toBeNull(); + expect(createPanelBox?.x ?? -1).toBeGreaterThanOrEqual(0); + expect((createPanelBox?.x ?? 0) + (createPanelBox?.width ?? 0)).toBeLessThanOrEqual(390); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); + await createPanel.getByRole("menuitem", { name: /^Agent\b/ }).click(); + const setup = page.locator(".agent-setup-page"); + const box = await setup.boundingBox(); expect(box).not.toBeNull(); expect(box?.x ?? -1).toBeGreaterThanOrEqual(0); expect(box?.y ?? -1).toBeGreaterThanOrEqual(0); - expect((box?.x ?? 0) + (box?.width ?? 0)).toBeLessThanOrEqual(390); - expect((box?.y ?? 0) + (box?.height ?? 0)).toBeLessThanOrEqual(844); - await expect(page.getByRole("button", { name: "Create Agent" })).toBeInViewport(); - await attachScreenshot(page, testInfo, "narrow-light-create-dialog"); - await page.keyboard.press("Escape"); + expect((box?.x ?? 0) + (box?.width ?? 0)).toBeLessThanOrEqual(390.5); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); + await expect(page.getByRole("button", { name: "Save Agent definition" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Request preview" })).toBeVisible(); + await expect(page.getByLabel("Text format")).toHaveValue("Text"); + await attachScreenshot(page, testInfo, "narrow-light-agent-setup"); + await page.getByRole("button", { name: "Back to Agents" }).click(); + await expect(globalCreate).toBeFocused(); await page.getByRole("button", { name: "Dark theme" }).click(); await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 5e3a667..b764183 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,9 +1,4 @@ -import { - Bot, - Layers3, - MessageSquare, - Settings2, -} from "lucide-react"; +import { Layers3, Settings2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AgentCoreError } from "@agents-core-web/agents-client"; @@ -20,6 +15,8 @@ import type { } from "@agents-core-web/agents-client"; import { ConnectionModal } from "./components/ConnectionModal"; +import { CreateMenu } from "./components/CreateMenu"; +import { ProductNavigation, type ProductView } from "./components/ProductNavigation"; import { StatusIcon } from "./components/StatusIcon"; import { ThemeMenu } from "./components/ThemeMenu"; import { useToast } from "./components/Toast"; @@ -31,6 +28,7 @@ import { requestAgentDetail, requestAgentUpdate, } from "./features/agents/agent-actions"; +import { EnvironmentsView } from "./features/environments/EnvironmentsView"; import { SessionsView, type SessionDetailState, @@ -99,7 +97,7 @@ import { waitForStreamReconnect, } from "./lib/stream-reconnect"; -type View = "sessions" | "agents" | "system"; +type View = ProductView | "system"; interface StreamConnection { sessionId: string | null; @@ -210,6 +208,10 @@ export function App() { () => new Map(), ); const [busy, setBusy] = useState(false); + const [agentCreateRequest, setAgentCreateRequest] = useState(null); + const [sessionCreateRequest, setSessionCreateRequest] = useState(null); + const agentCreateSequenceRef = useRef(0); + const sessionCreateSequenceRef = useRef(0); const selectedIdRef = useRef(selectedId); const sessionsRef = useRef(sessions); const itemsSessionIdRef = useRef(itemsSessionId); @@ -841,9 +843,10 @@ export function App() { const createAgent = async (input: CreateAgentInput) => { const agent = await run(() => core.createAgent(input), "Agent created."); - if (!agent || coreGeneration !== connectionGenerationRef.current) return; + if (!agent || coreGeneration !== connectionGenerationRef.current) return undefined; agentCollectionRevisionRef.current += 1; setAgents((current) => [agent, ...current]); + return agent; }; const retrieveAgent = async (agentId: string) => { @@ -1117,11 +1120,25 @@ export function App() { setConnectionOpen(false); }; - const navItems: Array<{ id: View; label: string; icon: typeof MessageSquare }> = [ - { id: "sessions", label: "Sessions", icon: MessageSquare }, - { id: "agents", label: "Agents", icon: Bot }, - { id: "system", label: "Architecture", icon: Layers3 }, - ]; + const openAgentSetup = () => { + setView("agents"); + agentCreateSequenceRef.current += 1; + setAgentCreateRequest(agentCreateSequenceRef.current); + }; + + const openSessionSetup = () => { + setView("sessions"); + sessionCreateSequenceRef.current += 1; + setSessionCreateRequest(sessionCreateSequenceRef.current); + }; + + const consumeAgentCreateRequest = useCallback((request: number) => { + setAgentCreateRequest((current) => current === request ? null : current); + }, []); + + const consumeSessionCreateRequest = useCallback((request: number) => { + setSessionCreateRequest((current) => current === request ? null : current); + }, []); return (
@@ -1135,43 +1152,18 @@ export function App() { Agents Core Web
-