fix: improve Code Agent long request UX

This commit is contained in:
unidesk-code-queue-runner
2026-05-24 01:04:52 +00:00
parent 282da213f7
commit 542d6de6dc
6 changed files with 545 additions and 28 deletions
+359 -17
View File
@@ -2505,7 +2505,7 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
/recordField\("workspace",\s*message\.workspace\)/u.test(messageEvidenceBody) &&
/recordField\("sandbox",\s*message\.sandbox\)/u.test(messageEvidenceBody) &&
/recordField\("capability",\s*message\.capabilityLevel\)/u.test(messageEvidenceBody) &&
/recordField\("session",\s*sessionSummary\(message\.session\)\)/u.test(messageEvidenceBody) &&
/recordField\("session",\s*sessionSummary\(message\.session\)\s*\?\?\s*message\.sessionId\)/u.test(messageEvidenceBody) &&
/recordField\("sessionMode",\s*message\.sessionMode\)/u.test(messageEvidenceBody) &&
/recordField\("sessionReuse",\s*sessionReuseSummary\(message\.sessionReuse\)\)/u.test(messageEvidenceBody) &&
/recordField\("longLivedGate",\s*longLivedSessionGateSummary\(message\.longLivedSessionGate\)\)/u.test(messageEvidenceBody) &&
@@ -2613,6 +2613,7 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
/function\s+isSourceFixtureCompletedChatMessage\s*\(/u.test(app) &&
/function\s+messageEvidencePanel\s*\(/u.test(app) &&
/function\s+messageAttributionPanel\s*\(/u.test(app) &&
/function\s+messagePendingContextPanel\s*\(/u.test(app) &&
/function\s+messageEvidenceSummary\s*\(/u.test(app) &&
/function\s+boundedEvidenceField\s*\(/u.test(app) &&
/codeAgentAttributionFromMessage/u.test(app) &&
@@ -2622,7 +2623,10 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) &&
/timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) &&
/HWLAB_CLOUD_WEB_CONFIG/u.test(app) &&
/失败时会保留输入供重试/u.test(app) &&
/正在处理这次 Code Agent 请求/u.test(app) &&
/旧 4500ms/u.test(app) &&
/结构化 blocker/u.test(app) &&
/sourceKind:\s*"PENDING"/u.test(app) &&
/Code Agent 请求超过/u.test(app) &&
/Code Agent SOURCE 回复/u.test(app) &&
/SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/u.test(app) &&
@@ -2631,6 +2635,7 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
/status-source/u.test(styles) &&
/message-user/u.test(styles) &&
/message-agent/u.test(styles) &&
/message-pending-context\s*\{/u.test(styles) &&
/message-m3-evidence\s*\{/u.test(styles) &&
/message-attribution\s*\{/u.test(styles) &&
/message-attribution-chip/u.test(styles) &&
@@ -2639,7 +2644,7 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
/message-evidence\s*\{/u.test(styles) &&
/message-evidence-chip/u.test(styles) &&
/width:\s*min\(100%,\s*780px\)/u.test(styles) &&
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : "source"/u.test(app) &&
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) &&
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(app)
);
}
@@ -4178,7 +4183,31 @@ async function inspectJourneyUi(page) {
const failedText = [...document.querySelectorAll(".message-card.status-failed")]
.map((element) => element.textContent ?? "")
.join("\n");
const pendingText = [...document.querySelectorAll(".message-card.status-running")]
.map((element) => element.textContent ?? "")
.join("\n");
const commandValue = document.querySelector("#command-input")?.value ?? "";
const pendingRows = [...document.querySelectorAll(".message-card.status-running .message-pending-row")].map((row) => {
const box = row.getBoundingClientRect();
return {
text: row.textContent?.replace(/\s+/gu, " ").trim() ?? "",
width: box.width,
scrollWidth: row.scrollWidth,
clientWidth: row.clientWidth,
contained: row.scrollWidth <= row.clientWidth + 2
};
});
const runningCardBoxes = [...document.querySelectorAll(".message-card.status-running")].map((card) => {
const box = card.getBoundingClientRect();
return {
width: box.width,
left: box.left,
right: box.right,
scrollWidth: card.scrollWidth,
clientWidth: card.clientWidth,
contained: box.left >= -1 && box.right <= window.innerWidth + 1 && card.scrollWidth <= card.clientWidth + 2
};
});
return {
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
@@ -4186,12 +4215,22 @@ async function inspectJourneyUi(page) {
helpHidden: document.querySelector('[data-view="help"]')?.hidden ?? null,
agentChatStatus: document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null,
inputCleared: commandValue === "",
retryInputPreserved: commandValue === "请用一句话说明当前 HWLAB 工作台可以做什么。",
retryInputPreserved: commandValue.trim().length > 0,
retryInputValue: commandValue,
completedMessageVisible: Boolean(document.querySelector(".message-card.status-completed")),
sourceMessageVisible: Boolean(document.querySelector(".message-card.status-source")),
failedMessageVisible: Boolean(document.querySelector(".message-card.status-failed")),
failedMessageHasTrace: /trace=trc_/u.test(failedText),
failedMessageHasChineseTimeout: /请求超过|请求超时|保留输入|重试/u.test(failedText),
runningMessageVisible: Boolean(document.querySelector(".message-card.status-running")),
pendingMessageText: pendingText.replace(/\s+/gu, " ").trim(),
pendingMessageChinese: /正在处理|后端处理中|不会把旧 4500ms|结构化 blocker|真实超时/u.test(pendingText),
pendingMessageHasTrace: /traceId\s*trc_|trace=trc_|trc_/u.test(pendingText),
pendingMessageHasConversation: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingMessageHasSession: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingContextRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardsContained: runningCardBoxes.length > 0 && runningCardBoxes.every((box) => box.contained),
messageCount: document.querySelectorAll(".message-card").length,
completedMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
@@ -4234,6 +4273,18 @@ async function inspectLegacyFailureWindow(page, { beforeMessageCount }) {
const failed = newMessages.filter((element) => element.classList.contains("status-failed"));
const latestAgent = [...newMessages].reverse().find((element) => element.classList.contains("message-agent"));
const agentChatStatus = document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null;
const pendingText = latestAgent?.classList.contains("status-running")
? latestAgent.textContent?.replace(/\s+/gu, " ").trim() ?? ""
: "";
const pendingRows = latestAgent
? [...latestAgent.querySelectorAll(".message-pending-row")].map((row) => ({
text: row.textContent?.replace(/\s+/gu, " ").trim() ?? "",
scrollWidth: row.scrollWidth,
clientWidth: row.clientWidth,
contained: row.scrollWidth <= row.clientWidth + 2
}))
: [];
const latestAgentBox = latestAgent?.getBoundingClientRect();
const pendingStillRunning =
latestAgent?.classList.contains("status-running") === true ||
agentChatStatus === "处理中" ||
@@ -4246,6 +4297,17 @@ async function inspectLegacyFailureWindow(page, { beforeMessageCount }) {
agentChatStatus,
failedMessageVisible: failed.length > 0,
runningMessageVisible: Boolean(latestAgent?.classList.contains("status-running")),
pendingChineseVisible: /正在处理|后端处理中|不会把旧 4500ms|结构化 blocker|真实超时/u.test(pendingText),
pendingTraceVisible: /traceId\s*trc_|trace=trc_|trc_/u.test(pendingText),
pendingConversationVisible: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingSessionVisible: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardContained: latestAgentBox
? latestAgentBox.left >= -1 &&
latestAgentBox.right <= window.innerWidth + 1 &&
latestAgent.scrollWidth <= latestAgent.clientWidth + 2
: false,
permanentFailureAround4500ms: failed.length > 0 && !pendingStillRunning
};
}, { beforeMessageCount, legacyFailureWindowMs });
@@ -4306,6 +4368,15 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
});
}
export async function runDevCloudWorkbenchSlowBlockerFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-slow-blocker-fixture-browser",
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
agentFixtureMode: "slow-blocker",
expectTimeout: false
});
}
export async function runDevCloudWorkbenchQuickPromptsFixtureSmoke() {
let chromium;
try {
@@ -4466,6 +4537,7 @@ async function runLocalAgentFixtureSmoke({
mode,
responseDelayMs = 0,
timeoutConfigMs = null,
agentFixtureMode = "success",
expectTimeout = false
} = {}) {
let chromium;
@@ -4505,7 +4577,7 @@ async function runLocalAgentFixtureSmoke({
};
}
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs });
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode });
let browser;
try {
browser = await chromium.launch({ headless: true });
@@ -4555,22 +4627,36 @@ async function runLocalAgentFixtureSmoke({
if (expectTimeout) {
await page.locator("#command-input").fill(codeAgentE2ePrompts[0].text);
await page.locator("#command-send").click();
await page.waitForTimeout(Math.min(legacyFailureWindowMs + 350, Math.max(80, timeoutConfigMs ?? 0) + 80));
await page.waitForFunction(
() => ["等待超时", "发送失败"].includes(document.querySelector("#agent-chat-status")?.textContent?.trim() ?? ""),
null,
{ timeout: 8000 }
);
} else {
for (const prompt of codeAgentE2ePrompts) {
const prompts = agentFixtureMode === "slow-blocker"
? [codeAgentE2ePrompts[1]]
: codeAgentE2ePrompts;
for (const prompt of prompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
if (agentFixtureMode === "success") {
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
}
}
const ui = await inspectJourneyUi(page);
const mobilePending = !expectTimeout && agentFixtureMode === "success"
? await inspectLocalAgentPendingViewport(browser, server.url, {
width: 390,
height: 844,
responseDelayMs,
timeoutConfigMs
})
: null;
responseSummary = response ? sanitizeAgentChatBody(responseBody, { httpStatus: response.status() }) : null;
const pass = expectTimeout
? (
@@ -4581,14 +4667,35 @@ async function runLocalAgentFixtureSmoke({
ui.failedMessageHasChineseTimeout &&
!ui.completedMessageVisible
)
: (
: agentFixtureMode === "slow-blocker"
? (
promptResults.length === 1 &&
promptResults[0]?.status === "blocked" &&
promptResults[0]?.classification?.blocker === "runner-blocked" &&
promptResults[0]?.legacyWindow?.permanentFailureAround4500ms === false &&
promptResults[0]?.legacyWindow?.pendingChineseVisible === true &&
ui.agentChatStatus === "Runner 受阻" &&
ui.failedMessageVisible &&
ui.failedMessageHasTrace &&
ui.retryInputPreserved &&
!ui.completedMessageVisible
)
: (
promptResults.every((result) =>
result.status === "pass" &&
result.response?.status === "completed" &&
result.response?.sourceKind === "SOURCE" &&
result.response?.hasReply === true &&
result.classification?.sourceOnly === true &&
result.legacyWindow?.permanentFailureAround4500ms === false
result.legacyWindow?.permanentFailureAround4500ms === false &&
result.legacyWindow?.runningMessageVisible === true &&
result.legacyWindow?.pendingChineseVisible === true &&
result.legacyWindow?.pendingTraceVisible === true &&
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) &&
ui.agentChatStatus === "SOURCE 回复" &&
ui.sourceMessageVisible &&
@@ -4598,6 +4705,7 @@ async function runLocalAgentFixtureSmoke({
ui.attributionMissingFieldsStructured &&
ui.attributionFallbackNotRunnerControl &&
ui.conversationFactsVisible &&
mobilePending?.pass === true &&
!ui.completedMessageVisible &&
!ui.failedMessageVisible
);
@@ -4619,6 +4727,8 @@ async function runLocalAgentFixtureSmoke({
status: pass ? "pass" : "blocked",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays failed/BLOCKED in the UI, preserves trace evidence, and keeps the user's input for retry."
: agentFixtureMode === "slow-blocker"
? "Local SOURCE fixture returns a delayed structured blocker and the conversation shows the Chinese reason with trace evidence while preserving the input."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: expectTimeout ? {
response: responseSummary,
@@ -4627,15 +4737,26 @@ async function runLocalAgentFixtureSmoke({
} : {
prompts: promptResults,
ui,
mobilePending,
networkEvents: agentResponses
}
}
];
if (!expectTimeout) {
if (!expectTimeout && agentFixtureMode === "success") {
checks.push({
id: "local-agent-fixture-no-4500ms-permanent-failure",
status: promptResults.every((result) => result.legacyWindow?.permanentFailureAround4500ms === false) ? "pass" : "blocked",
summary: "Local SOURCE fixture delays each Code Agent response beyond 4500ms and the frontend keeps the request pending instead of marking permanent failure.",
status: promptResults.every((result) =>
result.legacyWindow?.permanentFailureAround4500ms === false &&
result.legacyWindow?.runningMessageVisible === true &&
result.legacyWindow?.pendingChineseVisible === true &&
result.legacyWindow?.pendingTraceVisible === true &&
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) ? "pass" : "blocked",
summary: "Local SOURCE fixture delays each Code Agent response beyond 4500ms and the conversation shows Chinese pending state with trace/session evidence instead of permanent failure.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -4644,7 +4765,26 @@ async function runLocalAgentFixtureSmoke({
status: result.status,
legacyWindow: result.legacyWindow,
traceId: result.traceId
}))
})),
mobilePending
}
});
checks.push({
id: "local-agent-fixture-mobile-pending-layout",
status: mobilePending?.pass === true ? "pass" : "blocked",
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
observations: mobilePending
});
} else if (!expectTimeout && agentFixtureMode === "slow-blocker") {
checks.push({
id: "local-agent-fixture-slow-structured-blocker",
status: pass ? "pass" : "blocked",
summary: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of 发送失败, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
prompt: promptResults[0] ?? null,
ui
}
});
}
@@ -4672,6 +4812,7 @@ async function runLocalAgentFixtureSmoke({
safety: {
...staticSafety(),
localFixtureOnly: true,
agentFixtureMode,
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -4706,6 +4847,73 @@ async function runLocalAgentFixtureSmoke({
}
}
async function inspectLocalAgentPendingViewport(browser, url, { width, height, responseDelayMs, timeoutConfigMs }) {
const context = await browser.newContext({
viewport: { width, height },
deviceScaleFactor: 1,
isMobile: width <= 480
});
const page = await context.newPage();
try {
if (timeoutConfigMs !== null) {
await page.addInitScript((timeoutMs) => {
globalThis.HWLAB_CLOUD_WEB_CONFIG = {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentTimeoutMs: timeoutMs
}
};
}, timeoutConfigMs);
}
await page.goto(url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const beforeMessageCount = await page.locator(".message-card").count();
await page.locator("#command-input").fill(codeAgentE2ePrompts[1].text);
const responsePromise = page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
{ timeout: responseDelayMs + 10000 }
).catch(() => null);
await page.locator("#command-send").click();
await page.waitForTimeout(legacyFailureWindowMs + 350);
const legacyWindow = await inspectLegacyFailureWindow(page, { beforeMessageCount });
const ui = await inspectJourneyUi(page);
const response = await responsePromise;
return {
viewport: { width, height },
pass:
legacyWindow.permanentFailureAround4500ms === false &&
legacyWindow.runningMessageVisible === true &&
legacyWindow.pendingChineseVisible === true &&
legacyWindow.pendingTraceVisible === true &&
legacyWindow.pendingConversationVisible === true &&
legacyWindow.pendingSessionVisible === true &&
legacyWindow.pendingTraceCopyVisible === true &&
legacyWindow.pendingRowsContained === true &&
legacyWindow.runningCardContained === true &&
ui.runningCardsContained === true &&
ui.pendingContextRowsContained === true,
legacyWindow,
ui: {
agentChatStatus: ui.agentChatStatus,
runningMessageVisible: ui.runningMessageVisible,
pendingMessageChinese: ui.pendingMessageChinese,
pendingMessageHasTrace: ui.pendingMessageHasTrace,
pendingMessageHasConversation: ui.pendingMessageHasConversation,
pendingMessageHasSession: ui.pendingMessageHasSession,
pendingTraceCopyVisible: ui.pendingTraceCopyVisible,
pendingContextRowsContained: ui.pendingContextRowsContained,
runningCardsContained: ui.runningCardsContained
},
responseObserved: Boolean(response)
};
} finally {
await page.close().catch(() => null);
await context.close();
}
}
export async function runDevCloudWorkbenchMobileSmoke() {
let chromium;
try {
@@ -5880,6 +6088,140 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
const normalizedMessage = String(body?.message ?? "");
const isSkillListPrompt = /列出你能使用的所有skill/u.test(normalizedMessage);
const messageId = isSkillListPrompt ? "msg_source_fixture_browser_skills" : "msg_source_fixture_browser_simple";
if (options.agentFixtureMode === "slow-blocker") {
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "failed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-readonly-runner",
model: "read-only-tools",
backend: "local-source-fixture/slow-structured-blocker",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
workspace: "/workspace/hwlab",
sandbox: "read-only",
runner: {
kind: "hwlab-readonly-runner",
writeCapable: false,
codexStdio: false,
longLivedSession: true,
durableSession: false
},
capabilityLevel: "blocked",
sessionMode: "controlled-readonly-session-registry",
session: {
sessionId: conversationId,
conversationId,
status: "idle",
turn: 1,
lastTraceId: traceId
},
sessionReuse: {
conversationId,
sessionId: conversationId,
mapped: true,
reused: true,
turn: 1,
status: "idle"
},
error: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
message: "SOURCE slow structured blocker: no readable SKILL.md files were found.",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true,
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
}
},
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
},
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
retryable: true
}
],
toolCalls: [
{
name: "skills.discover",
status: "blocked",
exitCode: 2,
traceId,
stderrSummary: "skills_unavailable",
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
}
],
skills: {
status: "blocked",
items: [],
count: 0,
totalCount: 0,
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
]
},
runnerTrace: {
traceId,
runnerKind: "hwlab-readonly-runner",
sessionMode: "controlled-readonly-session-registry",
status: "blocked",
events: ["tool:skills.discover:blocked"]
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "controlled-readonly-session-registry",
turnCount: 1,
latestTraceId: traceId,
traceIds: [traceId],
latestSkills: {
status: "blocked",
count: 0,
totalCount: 0,
names: [],
blockers: ["skills_unavailable"]
},
recentToolCalls: [{ name: "skills.discover", status: "blocked" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
responseId: "rsp_source_fixture_slow_blocker",
redacted: true
}
});
return true;
}
const fixtureSkills = isSkillListPrompt
? {
status: "ready",