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:
2026-10-19 18:46:58 +08:00
parent 2c5bf15cce
commit ec23d44b02
131 changed files with 6663 additions and 1204 deletions
+63 -33
View File
@@ -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");