feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -42,7 +42,6 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
|
||||
const workspace = page.locator("#main-workspace");
|
||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
const agentHeader = agentPanel.locator(".agent-panel-header");
|
||||
const agentConversation = agentPanel.getByRole("log");
|
||||
const agentConversationScroll = agentPanel.locator(".agent-conversation-scroll");
|
||||
const agentControls = agentPanel.locator(".agent-panel-band").last();
|
||||
|
||||
@@ -63,28 +62,24 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
|
||||
await expect(agentPanel).toHaveCSS("border-right-width", "1px");
|
||||
await expectThemeColor(agentPanel, "border-right-color", "--surface-divider");
|
||||
await expect(agentPanel).toHaveCSS("box-shadow", "none");
|
||||
await expect(agentControls).toHaveCSS("border-top-width", "1px");
|
||||
await expectThemeColor(agentControls, "background-color", "--agent-dock-band");
|
||||
await expectThemeColor(agentPanel.locator("form.agent-panel-control"), "background-color", "--surface-reading");
|
||||
const capabilities = agentPanel.getByRole("list", { name: "Agent 分析能力" });
|
||||
await expect(capabilities.getByRole("listitem")).toHaveCount(4);
|
||||
await expect(capabilities).toHaveCSS("box-shadow", "none");
|
||||
for (const item of await capabilities.getByRole("listitem").all()) {
|
||||
await expect(item).toHaveCSS("border-right-width", "0px");
|
||||
await expect(item).toHaveCSS("border-bottom-width", "0px");
|
||||
}
|
||||
await expect(
|
||||
agentPanel.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
|
||||
).toHaveCSS("color", "rgb(51, 65, 85)");
|
||||
await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("color", "rgb(30, 41, 59)");
|
||||
await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("font-size", "14px");
|
||||
await expect(agentPanel).toHaveAttribute("data-agent-starting", "true");
|
||||
await expect(agentControls).toHaveCSS("border-top-width", "0px");
|
||||
await expect(agentControls).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expectAcrylicSurface(agentPanel.locator("form.agent-panel-control"));
|
||||
const inputBody = agentPanel.locator(".agent-panel-input-body");
|
||||
const inputTools = agentPanel.locator(".agent-panel-input-tools");
|
||||
await expectThemeColor(inputBody, "background-color", "--agent-dock-reading");
|
||||
await expectThemeColor(inputBody, "border-color", "--surface-border");
|
||||
await expect(inputBody).toHaveCSS("display", "block");
|
||||
await expect(inputTools).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
const startComposer = agentPanel.locator(".agent-panel-start-composer");
|
||||
const suggestions = agentPanel.getByRole("group", { name: "推荐问题" });
|
||||
await expect(suggestions.getByRole("button")).toHaveCount(3);
|
||||
await expect(agentPanel.getByRole("heading", { name: "我能帮你处理什么?" })).toBeVisible();
|
||||
await expect(agentPanel.locator(".agent-panel-main-content")).toBeHidden();
|
||||
|
||||
const panelBox = await agentPanel.boundingBox();
|
||||
const conversationBox = await agentConversation.boundingBox();
|
||||
const scrollBox = await agentConversationScroll.boundingBox();
|
||||
expect(panelBox).not.toBeNull();
|
||||
expect(conversationBox).not.toBeNull();
|
||||
expect(scrollBox).not.toBeNull();
|
||||
expect(panelBox!.x).toBe(0);
|
||||
const topBarBox = await topBar.boundingBox();
|
||||
expect(panelBox!.y).toBe(topBarBox!.y + topBarBox!.height);
|
||||
@@ -97,17 +92,27 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
|
||||
expect(headerBox!.y).toBe(topBarBox!.y + topBarBox!.height);
|
||||
expect(workspaceBox!.y).toBe(topBarBox!.y);
|
||||
await expect(topBar).toHaveCSS("box-shadow", "none");
|
||||
await expect(agentHeader).toHaveCSS("box-shadow", "none");
|
||||
await expectAcrylicSurface(agentHeader);
|
||||
await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
expect(await topBar.evaluate((element) => getComputedStyle(element).borderBottomColor)).toBe(
|
||||
await agentPanel.evaluate((element) => getComputedStyle(element).borderRightColor)
|
||||
);
|
||||
expect(Math.abs(conversationBox!.x - panelBox!.x)).toBeLessThanOrEqual(1);
|
||||
await expect(agentConversationScroll).toHaveCSS("scrollbar-gutter", "stable both-edges");
|
||||
|
||||
const beforeFocus = await getSurfacePresentation(agentPanel);
|
||||
await page.getByPlaceholder("输入调度问题,Agent 将通过后端会话流式响应").focus();
|
||||
await page.getByPlaceholder("询问供水问题,或描述需要处理的任务").focus();
|
||||
await expectThemeColor(inputBody, "border-color", "--field-focus-border");
|
||||
await expect(inputBody).toHaveCSS("box-shadow", "none");
|
||||
await expect(agentPanel.locator("form.agent-panel-control > [data-slot=input-group]")).toHaveCSS("border-width", "0px");
|
||||
await expect.poll(() => getSurfacePresentation(agentPanel)).toEqual(beforeFocus);
|
||||
await agentPanel.getByRole("button", { name: "选择对话归属" }).click();
|
||||
const projectMenu = page.getByRole("menu");
|
||||
await expect(projectMenu.getByRole("menuitem", { name: "个人会话" })).toBeVisible();
|
||||
await expect(projectMenu.getByRole("menuitem", { name: "当前项目" })).toBeVisible();
|
||||
await expect(projectMenu.getByRole("menuitem", { name: "新建项目" })).toBeVisible();
|
||||
await expect(projectMenu.getByRole("menuitem", { name: "切换项目" })).toHaveCount(0);
|
||||
await page.screenshot({ path: "test-results/agent-history-ui/project-menu.png" });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.screenshot({ path: "test-results/agent-history-ui/desktop-dock.png" });
|
||||
|
||||
for (const viewport of [
|
||||
@@ -125,6 +130,27 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
|
||||
path: `test-results/agent-history-ui/desktop-dock-${viewport.width}.png`
|
||||
});
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 1440, height: 640 });
|
||||
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
||||
for (const width of [400, 640]) {
|
||||
const box = (await handle.boundingBox())!;
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(width, box.y + box.height / 2);
|
||||
await page.mouse.up();
|
||||
await page.mouse.move(1000, 450);
|
||||
await expect(agentPanel).toHaveCSS("width", `${width}px`);
|
||||
expect(await startComposer.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||
expect(await inputTools.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||
const textarea = agentPanel.locator("textarea");
|
||||
await textarea.fill("核对近期供水压力变化,说明监测证据和需要现场核验的事项。".repeat(20));
|
||||
const composerBox = (await startComposer.boundingBox())!;
|
||||
expect(composerBox.y).toBeGreaterThanOrEqual(topBarBox!.height * 2);
|
||||
expect(composerBox.y + composerBox.height).toBeLessThanOrEqual(640);
|
||||
await textarea.fill("");
|
||||
await agentPanel.screenshot({ path: `test-results/agent-history-ui/desktop-material-${width}.png` });
|
||||
}
|
||||
});
|
||||
|
||||
test("desktop Agent portal menus keep local material and respect reduced transparency", async ({ page }) => {
|
||||
@@ -137,11 +163,12 @@ test("desktop Agent portal menus keep local material and respect reduced transpa
|
||||
const trigger = panel.getByRole("button", { name });
|
||||
await trigger.click();
|
||||
const menu = page.getByRole("menu");
|
||||
await expect(menu).toBeVisible();
|
||||
await waitForAnimations(menu);
|
||||
await expect(menu).toHaveAttribute("data-agent-presentation", "desktop-dock");
|
||||
expect(await menu.evaluate((element) => element.closest("aside"))).toBeNull();
|
||||
await expectThemeColor(menu, "background-color", "--agent-dock-overlay-surface");
|
||||
await expect(menu).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
|
||||
await expect(menu).toHaveCSS("background-image", "none");
|
||||
await expectAcrylicSurface(menu);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(trigger).toBeFocused();
|
||||
}
|
||||
@@ -158,7 +185,8 @@ test("desktop Agent portal menus keep local material and respect reduced transpa
|
||||
await page.keyboard.press("Escape");
|
||||
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||||
const history = panel.getByRole("region", { name: "Agent 历史记录" });
|
||||
await expectThemeColor(history, "background-color", "--palette-paper");
|
||||
await expectThemeColor(panel.locator(".agent-panel-header"), "background-color", "--palette-paper");
|
||||
await expect(panel.locator(".agent-panel-header")).toHaveCSS("backdrop-filter", "none");
|
||||
await expect(history).toHaveCSS("backdrop-filter", "none");
|
||||
});
|
||||
|
||||
@@ -178,8 +206,10 @@ test("desktop Agent history remains usable inside the docked navigation pane", a
|
||||
await waitForAnimations(history);
|
||||
await expect(historyButton).toHaveAttribute("aria-expanded", "true");
|
||||
await expectUnifiedHistoryStructure(history);
|
||||
await expectThemeColor(history, "background-color", "--agent-dock-overlay-surface");
|
||||
await expect(history).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
|
||||
await expectThemeColor(agentPanel.locator(".agent-panel-header"), "background-color", "--agent-dock-overlay-surface");
|
||||
await expectAcrylicSurface(agentPanel.locator(".agent-panel-header"));
|
||||
await expect(history).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(history).toHaveCSS("backdrop-filter", "none");
|
||||
|
||||
await history.getByRole("button", { name: "重命名对话" }).first().click();
|
||||
const renameInput = history.locator("input").first();
|
||||
@@ -339,12 +369,12 @@ test("long history titles keep rename and explicit delete usable across viewport
|
||||
}
|
||||
})
|
||||
);
|
||||
await page.route("**/api/v1/agent/chat/session/**", async (route) => {
|
||||
await page.route("**/api/v1/session/**", async (route) => {
|
||||
const request = route.request();
|
||||
const method = request.method();
|
||||
if (method === "OPTIONS") return route.fallback();
|
||||
const path = new URL(request.url()).pathname;
|
||||
if (method === "PATCH" && path === `/api/v1/agent/chat/session/${sessionId}/title`) {
|
||||
if (method === "PATCH" && path === `/api/v1/session/${sessionId}/title`) {
|
||||
const body = request.postDataJSON();
|
||||
title = body.title;
|
||||
writes.push({ method, path, body });
|
||||
@@ -353,7 +383,7 @@ test("long history titles keep rename and explicit delete usable across viewport
|
||||
json: {}
|
||||
});
|
||||
}
|
||||
if (method === "DELETE" && path === `/api/v1/agent/chat/session/${sessionId}`) {
|
||||
if (method === "DELETE" && path === `/api/v1/session/${sessionId}`) {
|
||||
deleted = true;
|
||||
writes.push({ method, path, body: null });
|
||||
return route.fulfill({
|
||||
@@ -444,7 +474,7 @@ test("long history titles keep rename and explicit delete usable across viewport
|
||||
expect(writes).toEqual([
|
||||
{
|
||||
method: "PATCH",
|
||||
path: `/api/v1/agent/chat/session/${sessionId}/title`,
|
||||
path: `/api/v1/session/${sessionId}/title`,
|
||||
body: { title: "城南压力复核(已复核)", is_title_manually_edited: true }
|
||||
}
|
||||
]);
|
||||
@@ -459,7 +489,7 @@ test("long history titles keep rename and explicit delete usable across viewport
|
||||
expect(writes).toHaveLength(2);
|
||||
expect(writes[1]).toEqual({
|
||||
method: "DELETE",
|
||||
path: `/api/v1/agent/chat/session/${sessionId}`,
|
||||
path: `/api/v1/session/${sessionId}`,
|
||||
body: null
|
||||
});
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
Reference in New Issue
Block a user