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
+19 -37
View File
@@ -1,7 +1,9 @@
import { responseFixtureWriter } from "./support/responses-fixture";
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { expect, test } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api";
import { expectThemeColor } from "./support/theme-colors";
const STREAM_CHUNK_SIZE = 18;
const STREAM_CHUNK_DELAY_MS = 20;
@@ -17,16 +19,14 @@ test.beforeAll(async () => {
streamServer = createServer((_request, response) => {
response.writeHead(200, {
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
"X-Vercel-AI-UI-Message-Stream": "v1"
"Cache-Control": "no-cache, no-transform",
"Content-Type": "text/event-stream"
});
const content = createLongMarkdownResponse();
const chunks = splitIntoCodePointChunks(content, STREAM_CHUNK_SIZE);
const send = (chunk: Record<string, unknown>) => {
response.write(`data: ${JSON.stringify(chunk)}\n\n`);
};
const encode = responseFixtureWriter();
const send = (chunk: Record<string, unknown>) => { response.write(encode(chunk)); };
send({ type: "start", messageId: "assistant-stream-stability" });
send({
@@ -186,7 +186,7 @@ test("stream completion and the plan card remain positionally stable", async ({
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
TJWATER_MAP_URL: "https://stream-map.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_MAPBOX_ACCESS_TOKEN: "",
@@ -195,7 +195,7 @@ test("stream completion and the plan card remain positionally stable", async ({
})
);
await page.route("https://stream-map.invalid/**", (route) => route.fulfill({ status: 204 }));
await page.route("**/api/v1/agent/chat/**", async (route) => {
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
@@ -210,7 +210,7 @@ test("stream completion and the plan card remain positionally stable", async ({
const path = new URL(route.request().url()).pathname;
if (path.endsWith("/stream")) {
if (path.endsWith("/responses")) {
await route.continue({ url: streamServerUrl });
return;
}
@@ -244,7 +244,7 @@ test("stream completion and the plan card remain positionally stable", async ({
page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first()
).toContainText("测试模型", { timeout: 10_000 });
const prompt = page
.getByPlaceholder(/输入调度问题/)
.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" })
.filter({ visible: true })
.first();
await expect(prompt).toBeVisible();
@@ -311,7 +311,7 @@ test("stream completion and the plan card remain positionally stable", async ({
await sendButton.click();
const userMessage = page.locator(".agent-panel-user-message");
const assistantMessage = page.locator(".agent-panel-message").first();
await expect(userMessage).toHaveCSS("background-color", "oklch(0.965 0.018 255)");
await expectThemeColor(userMessage, "background-color", "--agent-user-message-surface");
expect(
await userMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
).not.toBe(
@@ -323,26 +323,10 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect
.poll(() => assistantMessage.evaluate((element) => getComputedStyle(element).boxShadow))
.toBe(await userMessage.evaluate((element) => getComputedStyle(element).boxShadow));
const operationalBrief = page.locator(".agent-panel-control").filter({
has: page.getByRole("button", { name: "预览影响", exact: true })
});
await expect(operationalBrief).toBeVisible();
await expect(operationalBrief).toContainText("Agent 分析中");
await expect(operationalBrief.getByRole("button", { name: "预览影响" })).toBeDisabled();
await expect(operationalBrief.getByRole("button", { name: "检查证据" })).toBeDisabled();
await expect(operationalBrief).toHaveCSS("position", "static");
await expect(operationalBrief).toHaveCSS("border-radius", "12px");
// Desktop uses an inline brief above the conversation, not the legacy floating overlay.
await expect(page.locator(".agent-panel-operational-brief")).toHaveCount(0);
await expectThemeColor(page.locator("[data-agent-conversation-state=active]"), "background-color", "--agent-dock-reading");
await expect(page.locator(".agent-panel-shell")).toHaveClass(/surface-dock/);
await expect(page.locator(".agent-panel-operational-float")).toHaveCount(0);
await expect
.poll(() =>
operationalBrief.evaluate((element) => {
const conversation = document.querySelector(".agent-panel-conversation")!;
return conversation.getBoundingClientRect().top - element.getBoundingClientRect().bottom;
})
)
.toBeGreaterThanOrEqual(0);
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
await expect(liveProgress.locator("li")).toHaveCount(3);
@@ -470,9 +454,9 @@ test("stream completion and the plan card remain positionally stable", async ({
const expandedLiveProgress = page.getByRole("list", { name: "全部执行进度" });
await expect(expandedLiveProgress.locator("li")).toHaveCount(4);
await expect(expandedLiveProgress.locator(".status-badge")).toHaveCount(0);
await expect(expandedLiveProgress.locator('[data-progress-status="running"]').first()).toHaveCSS(
"color",
"oklch(0.546 0.245 262.881)"
await expectThemeColor(
expandedLiveProgress.locator('[data-progress-status="running"]').first(),
"color", "--action-blue-hover"
);
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
await expect(
@@ -494,10 +478,8 @@ test("stream completion and the plan card remain positionally stable", async ({
resumeStream();
await expect(
page.getByText("分析完成", { exact: true }).filter({ visible: true }).first()
).toBeVisible({
timeout: 10_000
});
page.getByRole("button", { name: "发送 Agent 指令" }).filter({ visible: true }).first()
).toBeVisible({ timeout: 10_000 });
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
await expect(
@@ -618,7 +600,7 @@ test("stream completion and the plan card remain positionally stable", async ({
expect(pressedScrollButtonBox.y).toBe(scrollButtonBox.y + 1);
await page.mouse.move(scrollButtonBox.x - 20, scrollButtonBox.y);
await page.mouse.up();
await expect(scrollToLatestButton).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
await expectThemeColor(scrollToLatestButton, "background-color", "--agent-dock-overlay-surface");
await expect
.poll(() =>
scrollToLatestButton.evaluate((element) => getComputedStyle(element).backdropFilter)