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
+8 -7
View File
@@ -1,3 +1,4 @@
import { responsesFixture } from "./support/responses-fixture";
import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
@@ -99,7 +100,7 @@ for (const width of [1440, 375, 320]) {
data: { content: "仍需用户补充现场核验资料。", ui_block_ids: ["pressure"] }
}
];
await page.route("**/api/v1/agent/chat/session/browser-session-1", async (route) => {
await page.route("**/api/v1/session/browser-session-1", async (route) => {
if (route.request().method() !== "GET") return route.fallback();
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
@@ -121,16 +122,16 @@ for (const width of [1440, 375, 320]) {
});
});
const prompts: string[] = [];
await page.route("**/api/v1/agent/chat/stream", async (route) => {
await page.route("**/api/v1/responses", async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback();
const body = route.request().postDataJSON() as {
messages: Array<{ role: string; parts: Array<{ type: string; text?: string }> }>;
input: Array<{ role: string; content: Array<{ type: string; text?: string }> }>;
};
const text =
body.messages
body.input
.filter((m) => m.role === "user")
.at(-1)
?.parts.filter((p) => p.type === "text")
?.content.filter((p) => p.type === "input_text")
.map((p) => p.text)
.join("") ?? "";
prompts.push(text);
@@ -160,9 +161,9 @@ for (const width of [1440, 375, 320]) {
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/event-stream",
"X-Vercel-AI-UI-Message-Stream": "v1"
"Cache-Control": "no-cache"
},
body: chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")
body: responsesFixture(chunks)
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });