fix: improve Code Agent long request UX
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
parseSmokeArgs,
|
||||
runDevCloudWorkbenchLayoutSmoke,
|
||||
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
|
||||
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke,
|
||||
runDevCloudWorkbenchTimeoutFixtureSmoke,
|
||||
sanitizeAgentChatBody
|
||||
@@ -671,6 +672,32 @@ test("local Code Agent timeout fixture keeps failed UI state, trace evidence, an
|
||||
assert.equal(timeoutCheck.observations.ui.completedMessageVisible, false);
|
||||
});
|
||||
|
||||
test("local slow structured blocker fixture preserves trace, pending state, and retry input", async () => {
|
||||
const report = await runDevCloudWorkbenchSlowBlockerFixtureSmoke({
|
||||
responseDelayMs: 5200
|
||||
});
|
||||
|
||||
if (report.status === "skip") {
|
||||
assert.match(report.summary, /Playwright is unavailable/u);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2));
|
||||
assert.equal(report.evidenceLevel, "SOURCE");
|
||||
assert.equal(report.devLive, false);
|
||||
const blockerCheck = report.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
|
||||
assert.equal(blockerCheck?.status, "pass");
|
||||
assert.equal(blockerCheck.observations.ui.agentChatStatus, "Runner 受阻");
|
||||
assert.equal(blockerCheck.observations.ui.failedMessageVisible, true);
|
||||
assert.equal(blockerCheck.observations.ui.failedMessageHasTrace, true);
|
||||
assert.equal(blockerCheck.observations.ui.retryInputPreserved, true);
|
||||
assert.equal(blockerCheck.observations.ui.retryInputValue, "列出你能使用的所有skill");
|
||||
assert.equal(blockerCheck.observations.prompt.legacyWindow.permanentFailureAround4500ms, false);
|
||||
assert.equal(blockerCheck.observations.prompt.legacyWindow.pendingChineseVisible, true);
|
||||
assert.equal(blockerCheck.observations.prompt.response.error.code, "skills_unavailable");
|
||||
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
|
||||
});
|
||||
|
||||
test("Code Agent quick prompt fixture fills input, does not autosend writes, and fits mobile", async () => {
|
||||
const report = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
|
||||
if (report.status === "skip") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -610,6 +610,8 @@ function initCommandBar() {
|
||||
const value = el.commandInput.value.trim();
|
||||
if (!value || state.chatPending) return;
|
||||
const traceId = nextProtocolId("trc");
|
||||
const activeConversationId = state.conversationId ?? nextProtocolId("cnv");
|
||||
state.conversationId = activeConversationId;
|
||||
const userMessage = {
|
||||
id: nextProtocolId("msg"),
|
||||
role: "user",
|
||||
@@ -617,15 +619,19 @@ function initCommandBar() {
|
||||
text: value,
|
||||
status: "sent",
|
||||
traceId,
|
||||
conversationId: activeConversationId,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
const pendingMessage = {
|
||||
id: nextProtocolId("msg"),
|
||||
role: "agent",
|
||||
title: "Code Agent 处理中",
|
||||
text: `正在调用真实 Code Agent / Codex 后端,复杂问题可能需要 1-2 分钟;超时上限 ${Math.round(CODE_AGENT_TIMEOUT_MS / 1000)} 秒,失败时会保留输入供重试。`,
|
||||
text: `正在处理这次 Code Agent 请求;复杂问题可能需要 1-2 分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`,
|
||||
status: "running",
|
||||
traceId,
|
||||
conversationId: activeConversationId,
|
||||
sessionId: state.sessionId,
|
||||
sourceKind: "PENDING",
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
state.chatMessages.push(userMessage, pendingMessage);
|
||||
@@ -638,7 +644,7 @@ function initCommandBar() {
|
||||
renderRecords(state.liveSurface);
|
||||
|
||||
try {
|
||||
const result = await sendAgentMessage(value, state.conversationId, traceId);
|
||||
const result = await sendAgentMessage(value, activeConversationId, traceId);
|
||||
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
|
||||
state.sessionId = result.sessionId || state.sessionId || state.conversationId;
|
||||
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
|
||||
@@ -658,6 +664,7 @@ function initCommandBar() {
|
||||
status,
|
||||
traceId: result.traceId,
|
||||
conversationId: result.conversationId || result.sessionId || state.conversationId,
|
||||
sessionId: result.sessionId || result.session?.sessionId || result.sessionReuse?.sessionId || state.sessionId,
|
||||
messageId: result.messageId,
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
@@ -1972,13 +1979,14 @@ function codeAgentRecordCards() {
|
||||
infoCard({
|
||||
title: `${roleLabel(message.role)} ${statusLabel(message.status)}`,
|
||||
detail: [
|
||||
sourceKindLabel(message.status === "failed" ? "blocked" : "source"),
|
||||
recordField("conversation", state.conversationId),
|
||||
sourceKindLabel(message.status === "running" ? "pending" : message.status === "failed" ? "blocked" : "source"),
|
||||
recordField("conversation", message.conversationId ?? state.conversationId),
|
||||
recordField("trace", message.traceId),
|
||||
recordField("session", message.sessionId ?? sessionSummary(message.session)),
|
||||
recordField("message", message.messageId),
|
||||
message.error?.message ? `失败原因=${safeFailureReason(message.error.message)}` : null
|
||||
].filter(Boolean).join(" / "),
|
||||
tone: message.status === "failed" ? "blocked" : "source"
|
||||
tone: message.status === "running" ? "pending" : message.status === "failed" ? "blocked" : "source"
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -2312,7 +2320,7 @@ function renderAgentChatStatus(status, result = null) {
|
||||
blocked: "服务受阻"
|
||||
};
|
||||
el.agentChatStatus.textContent = agentStatusLabel(status, result, labels);
|
||||
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`;
|
||||
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : status === "running" ? "pending" : "source")}`;
|
||||
el.commandInput.disabled = status === "running";
|
||||
el.commandSend.disabled = status === "running";
|
||||
el.commandSend.textContent = status === "running" ? "发送中" : "发送";
|
||||
@@ -2644,6 +2652,8 @@ function messageCard(message) {
|
||||
article.append(textSpan(roleLabel(message.role), "message-role"));
|
||||
article.append(textSpan(message.title, "message-title"));
|
||||
article.append(textSpan(message.text, "message-copy"));
|
||||
const pendingContext = messagePendingContextPanel(message);
|
||||
if (pendingContext) article.append(pendingContext);
|
||||
const attribution = messageAttributionPanel(message);
|
||||
if (attribution) article.append(attribution);
|
||||
const capabilityFacts = codeAgentCapabilityFactsPanel(message);
|
||||
@@ -2656,6 +2666,7 @@ function messageCard(message) {
|
||||
}
|
||||
|
||||
function messageAttributionPanel(message) {
|
||||
if (message.status === "running") return null;
|
||||
const attribution = codeAgentAttributionFromMessage(message);
|
||||
if (!attribution) return null;
|
||||
const section = document.createElement("section");
|
||||
@@ -2684,6 +2695,7 @@ function messageAttributionPanel(message) {
|
||||
}
|
||||
|
||||
function codeAgentCapabilityFactsPanel(message) {
|
||||
if (message.status === "running") return null;
|
||||
const facts = codeAgentFactsFromMessage(message);
|
||||
if (!facts) return null;
|
||||
const section = document.createElement("section");
|
||||
@@ -2717,6 +2729,36 @@ function codeAgentCapabilityFactsPanel(message) {
|
||||
return section;
|
||||
}
|
||||
|
||||
function messagePendingContextPanel(message) {
|
||||
if (message.status !== "running") return null;
|
||||
const fields = [
|
||||
["状态", "后端处理中"],
|
||||
["traceId", message.traceId],
|
||||
["conversation", message.conversationId],
|
||||
["session", message.sessionId ?? "等待后端分配"],
|
||||
["timeout", `${CODE_AGENT_TIMEOUT_MS}ms`]
|
||||
];
|
||||
const section = document.createElement("section");
|
||||
section.className = "message-pending-context tone-border-pending";
|
||||
const header = document.createElement("div");
|
||||
header.className = "message-pending-head";
|
||||
header.append(badge("处理中", "pending"), textSpan("请求仍在同一会话上下文中等待后端结果。", "message-pending-summary"));
|
||||
const list = document.createElement("div");
|
||||
list.className = "message-pending-grid";
|
||||
for (const [label, value] of fields) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const item = document.createElement("div");
|
||||
item.className = "message-pending-row";
|
||||
item.append(textSpan(label, "message-pending-key"), textSpan(value, "message-pending-value mono"));
|
||||
if (label === "traceId" || label === "conversation" || label === "session") {
|
||||
item.append(copyButton(value, label));
|
||||
}
|
||||
list.append(item);
|
||||
}
|
||||
section.append(header, list);
|
||||
return section;
|
||||
}
|
||||
|
||||
function codeAgentFactsHeader(facts) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "code-agent-facts-head";
|
||||
@@ -2766,7 +2808,7 @@ function messageEvidencePanel(message) {
|
||||
if (fields.length === 0) return null;
|
||||
const details = document.createElement("details");
|
||||
details.className = "message-evidence";
|
||||
details.open = message.status === "completed" || message.status === "source";
|
||||
details.open = message.status === "running" || message.status === "completed" || message.status === "source" || message.status === "failed";
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "message-meta";
|
||||
summary.textContent = messageEvidenceSummary(message, fields);
|
||||
@@ -2778,11 +2820,29 @@ function messageEvidencePanel(message) {
|
||||
}
|
||||
|
||||
function messageEvidenceSummary(message, fields) {
|
||||
const sourceKind = message.sourceKind ?? (message.status === "completed" ? "DEV-LIVE" : message.status === "failed" ? "BLOCKED" : "SOURCE");
|
||||
const sourceKind = message.sourceKind ?? (
|
||||
message.status === "running"
|
||||
? "PENDING"
|
||||
: message.status === "completed"
|
||||
? "DEV-LIVE"
|
||||
: message.status === "failed"
|
||||
? "BLOCKED"
|
||||
: "SOURCE"
|
||||
);
|
||||
return `证据 ${sourceKind} / ${fields.slice(0, 3).join(" / ")}`;
|
||||
}
|
||||
|
||||
function messageEvidenceFields(message) {
|
||||
if (message.status === "running") {
|
||||
return [
|
||||
recordField("source", message.sourceKind ?? "PENDING"),
|
||||
recordField("status", "running"),
|
||||
recordField("conversation", message.conversationId),
|
||||
recordField("trace", message.traceId),
|
||||
recordField("session", message.sessionId ?? "pending"),
|
||||
recordField("timeout", `${CODE_AGENT_TIMEOUT_MS}ms`)
|
||||
].filter(Boolean);
|
||||
}
|
||||
const attribution = codeAgentAttributionFromMessage(message);
|
||||
return [
|
||||
...(attribution ? attribution.fields.map((field) => field.chip) : []),
|
||||
@@ -2794,7 +2854,7 @@ function messageEvidenceFields(message) {
|
||||
recordField("workspace", message.workspace),
|
||||
recordField("sandbox", message.sandbox),
|
||||
recordField("capability", message.capabilityLevel),
|
||||
recordField("session", sessionSummary(message.session)),
|
||||
recordField("session", sessionSummary(message.session) ?? message.sessionId),
|
||||
recordField("sessionMode", message.sessionMode),
|
||||
recordField("sessionReuse", sessionReuseSummary(message.sessionReuse)),
|
||||
recordField("implementation", message.implementationType),
|
||||
|
||||
@@ -206,6 +206,27 @@ test("maps stateless one-shot runner separately from long-lived sessions", () =>
|
||||
assert.equal(summary.lastTraceId, "trc_one_shot");
|
||||
});
|
||||
|
||||
test("summarizes active Code Agent running request as pending with trace and session context", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
role: "agent",
|
||||
status: "running",
|
||||
sourceKind: "PENDING",
|
||||
traceId: "trc_pending_heavy_skills",
|
||||
conversationId: "cnv_pending_heavy_skills",
|
||||
sessionId: "cnv_pending_heavy_skills"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(summary.kind, "unverified");
|
||||
assert.equal(summary.label, "探测中");
|
||||
assert.equal(summary.tone, "pending");
|
||||
assert.equal(summary.codeAgentStatus, "running");
|
||||
assert.equal(summary.sessionId, "cnv_pending_heavy_skills");
|
||||
assert.equal(summary.lastTraceId, "trc_pending_heavy_skills");
|
||||
assert.equal(summary.readinessBlockers.length, 0);
|
||||
});
|
||||
|
||||
test("maps HWLAB Skill CLI API control to distinct green state when ready", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
runDevCloudWorkbenchMobileSmoke,
|
||||
runDevCloudWorkbenchAuthFixtureSmoke,
|
||||
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
|
||||
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke
|
||||
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import { gateSummary } from "../gate-summary.mjs";
|
||||
@@ -152,6 +153,9 @@ const mobileOuterScrollSmokeCheck = mobileWorkbenchSmoke.checks.find((check) =>
|
||||
const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-help-markdown-route");
|
||||
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
|
||||
const localAgentNo4500Check = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-no-4500ms-permanent-failure");
|
||||
const localAgentMobilePendingCheck = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-mobile-pending-layout");
|
||||
const slowBlockerFixtureSmoke = await runDevCloudWorkbenchSlowBlockerFixtureSmoke();
|
||||
const slowBlockerCheck = slowBlockerFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
|
||||
const quickPromptsFixtureSmoke = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
|
||||
const quickPromptsFillCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-fill-input");
|
||||
const quickPromptsNoAutosendCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-write-no-autosend");
|
||||
@@ -218,6 +222,11 @@ assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFix
|
||||
assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(localAgentFixtureSmoke.devLive, false);
|
||||
assert.equal(localAgentNo4500Check?.status, "pass", "local Code Agent fixture must prove the 4500ms browser timeout path is not permanent");
|
||||
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local Code Agent fixture must keep mobile pending state visible with trace/session context");
|
||||
assert.equal(slowBlockerFixtureSmoke.status, "pass", JSON.stringify(slowBlockerFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(slowBlockerFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(slowBlockerFixtureSmoke.devLive, false);
|
||||
assert.equal(slowBlockerCheck?.status, "pass", "slow structured blocker fixture must show Chinese reason, trace, retry input, and no 4500ms permanent failure");
|
||||
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(quickPromptsFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(quickPromptsFixtureSmoke.devLive, false);
|
||||
@@ -760,7 +769,7 @@ assert.match(app, /recordField\("provider", message\.provider\)/);
|
||||
assert.match(app, /recordField\("operationId", operationIdSummary\(message\)\)/);
|
||||
assert.match(app, /recordField\("audit", auditSummary\(message\)\)/);
|
||||
assert.match(app, /recordField\("evidence", evidenceSummary\(message\)\)/);
|
||||
assert.match(app, /recordField\("session", sessionSummary\(message\.session\)\)/);
|
||||
assert.match(app, /recordField\("session", sessionSummary\(message\.session\)\s*\?\?\s*message\.sessionId\)/);
|
||||
assert.match(app, /recordField\("sessionMode", message\.sessionMode\)/);
|
||||
assert.match(app, /recordField\("sessionReuse", sessionReuseSummary\(message\.sessionReuse\)\)/);
|
||||
assert.match(app, /recordField\("implementation", message\.implementationType\)/);
|
||||
@@ -843,6 +852,7 @@ assert.match(styles, /\.message-card\.status-completed\s*{/);
|
||||
assert.match(styles, /\.message-card\.status-source\s*{/);
|
||||
assert.match(styles, /\.message-user\s*{/);
|
||||
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
|
||||
assert.match(styles, /\.message-pending-context\s*{/);
|
||||
assert.match(styles, /\.message-attribution\s*{/);
|
||||
assert.match(styles, /\.message-attribution-chip,\s*\n\.message-evidence-chip\s*{/);
|
||||
assert.match(styles, /\.message-attribution-chip\.is-missing\s*{/);
|
||||
@@ -891,7 +901,11 @@ assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/);
|
||||
assert.match(app, /CODE_AGENT_TIMEOUT_MS/);
|
||||
assert.match(app, /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
|
||||
assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/);
|
||||
assert.match(app, /失败时会保留输入供重试/);
|
||||
assert.match(app, /sourceKind:\s*"PENDING"/);
|
||||
assert.match(app, /旧 4500ms/);
|
||||
assert.match(app, /function messagePendingContextPanel/);
|
||||
assert.match(app, /message-pending-context/);
|
||||
assert.match(app, /结构化 blocker/);
|
||||
assert.match(app, /Code Agent 请求超过/);
|
||||
assert.match(app, /agentFailurePresentation/);
|
||||
assert.match(app, /agentStatusLabel/);
|
||||
|
||||
@@ -887,6 +887,7 @@ h3 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-pending-context,
|
||||
.message-attribution,
|
||||
.code-agent-facts,
|
||||
.message-m3-evidence {
|
||||
@@ -898,6 +899,10 @@ h3 {
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.message-pending-context {
|
||||
background: rgba(55, 45, 24, 0.54);
|
||||
}
|
||||
|
||||
.message-attribution {
|
||||
background: rgba(20, 24, 23, 0.76);
|
||||
border-left-width: 3px;
|
||||
@@ -912,6 +917,7 @@ h3 {
|
||||
background: rgba(15, 17, 16, 0.44);
|
||||
}
|
||||
|
||||
.message-pending-head,
|
||||
.message-attribution-head,
|
||||
.code-agent-facts-head {
|
||||
min-width: 0;
|
||||
@@ -921,6 +927,13 @@ h3 {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.message-pending-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-pending-summary,
|
||||
.message-attribution-summary,
|
||||
.code-agent-facts-summary {
|
||||
min-width: 0;
|
||||
@@ -929,6 +942,40 @@ h3 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-pending-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.message-pending-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, auto) minmax(0, 1fr) auto;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 5px 6px;
|
||||
background: rgba(24, 27, 24, 0.68);
|
||||
border: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.message-pending-key,
|
||||
.message-pending-value {
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-pending-key {
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.message-pending-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.message-m3-head {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
@@ -2041,12 +2088,18 @@ tbody tr:last-child td {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.message-pending-context,
|
||||
.message-attribution,
|
||||
.code-agent-facts,
|
||||
.message-m3-evidence {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.message-pending-grid,
|
||||
.message-pending-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.message-m3-rows {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user