fix: polish degraded workbench mobile UX

This commit is contained in:
Code Queue Review
2026-05-23 03:14:58 +00:00
parent 0fe2b2c12a
commit 6339eebef7
5 changed files with 149 additions and 25 deletions
+67 -4
View File
@@ -58,7 +58,14 @@ const defaultHomepageForbiddenTerms = Object.freeze([
"BLOCKED",
"db-live",
"m3-hardware-loop-runtime",
"执行轨迹"
"执行轨迹",
"runtime_durable_adapter_query_blocked",
"OPENAI_API_KEY",
"Secret",
"secretRef",
"hwlab-code-agent-provider",
"m3-route-required",
"waiting-for-dev-live"
]);
const diagnosticsRequiredTerms = Object.freeze([
@@ -367,9 +374,19 @@ function runStaticSmoke() {
evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"]
});
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider credential blockers and only real completed replies can become DEV-LIVE.", {
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider blockers without exposing credential internals and only real completed replies can become DEV-LIVE.", {
blocker: "observability_blocker",
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard"]
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
});
addCheck(checks, blockers, "default-workspace-sanitized", defaultWorkspaceIsSanitized(files), "Default user workspace hides raw blocker codes, internal route markers, and credential reference material.", {
blocker: "observability_blocker",
evidence: defaultHomepageForbiddenTerms
});
addCheck(checks, blockers, "route-control-stable-dimensions", hasStableRouteControls(files), "Route controls and status tags have stable dimensions, readable wrapping, and usable labels on small screens.", {
blocker: "runtime_blocker",
evidence: ["rail-button min-width/min-height", "state-tag wrapping", "side-tab stable labels", "mobile message evidence column"]
});
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed Code Agent replies expose provider/model/trace/conversation evidence and reject echo/mock/stub completions.", {
@@ -377,7 +394,7 @@ function runStaticSmoke() {
evidence: requiredCodeAgentEvidenceTerms
});
addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and provider_unavailable states as distinct conversation replies with bounded evidence.", {
addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and blocked provider states as distinct conversation replies with bounded evidence.", {
blocker: "observability_blocker",
evidence: [
"DEV-LIVE 回复",
@@ -1053,6 +1070,27 @@ function defaultHomepageHidesBackstage({ html }) {
return defaultHomepageForbiddenTerms.every((term) => !visibleShellHtml.includes(term));
}
function defaultWorkspaceIsSanitized({ html, app }) {
const workspaceHtml = sectionSource(html, "workspace");
const defaultShell = `${beforeSection(html, "help")}\n${workspaceHtml}`;
const userFacingBodies = [
functionBody(app, "codeAgentStatusMessage"),
functionBody(app, "codeAgentPromptText"),
functionBody(app, "codeAgentControlSummary"),
functionBody(app, "codeAgentBlockedSummary"),
functionBody(app, "codeAgentAvailabilitySummary"),
functionBody(app, "failureMessage"),
functionBody(app, "renderWiringList")
].join("\n");
return (
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live/u.test(defaultShell) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u.test(userFacingBodies) &&
/服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) &&
/Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) &&
/等待可信记录/u.test(functionBody(app, "renderWiringList"))
);
}
function hasCompletePrimaryNavigation(html) {
const navLabelsPresent = ["工作台", "内部复核", "使用说明", "资源树"].every((label) => html.includes(`>${label}<`));
const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`));
@@ -1142,6 +1180,7 @@ function trustedRecordGroups({ html, app, styles }) {
/function\s+evidenceCard\s*\(/u.test(app) &&
/function\s+recordGroup\s*\(/u.test(app) &&
/function\s+recordField\s*\(/u.test(app) &&
/function\s+safeFailureReason\s*\(/u.test(app) &&
/function\s+sourceKindLabel\s*\(/u.test(app) &&
/function\s+liveFailureCard\s*\(/u.test(app) &&
/state\.liveSurface/u.test(app) &&
@@ -1156,6 +1195,7 @@ function trustedRecordGroups({ html, app, styles }) {
/gateSummary\.operations\.map/u.test(app) &&
/gateSummary\.auditEvents\.slice\(0,\s*4\)\.map/u.test(app) &&
/gateSummary\.evidenceRecords\.map/u.test(app) &&
/safeFailureReason\(message\.error\.message\)/u.test(app) &&
/message\.status === "failed" \? "blocked" : "source"/u.test(app) &&
!/message\.status === "completed"\s*\?\s*"dev-live"/u.test(app)
);
@@ -1201,6 +1241,19 @@ function hasMobileWorkbenchLayoutContract({ html, app, styles }) {
);
}
function hasStableRouteControls({ html, styles }) {
return (
["工作台", "内部复核", "使用说明", "资源树", "控制", "接线", "可信记录"].every((label) => html.includes(`>${label}<`)) &&
/\.rail-button\s*\{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/su.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.rail-button\s*\{[\s\S]*?min-height:\s*42px;/u.test(styles) &&
/\.side-tab\s*\{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/su.test(styles) &&
/\.(?:status-dot|state-tag|badge)[^{]*\{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
/\.probe-card\s*\{[^}]*min-width:\s*0;/su.test(styles) &&
/\.probe-card strong\s*\{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-evidence\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
);
}
function hasSameOriginReadOnlyBoundary(app) {
const rpcCalls = [...app.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]).sort();
return (
@@ -1257,9 +1310,18 @@ function hasCodeAgentChatContract({ html, app }) {
function hasCodeAgentReadinessVisibility({ html, app }) {
const source = `${html}\n${app}`;
const userFacingBodies = [
functionBody(app, "codeAgentStatusMessage"),
functionBody(app, "codeAgentPromptText"),
functionBody(app, "codeAgentControlSummary"),
functionBody(app, "codeAgentBlockedSummary"),
functionBody(app, "codeAgentAvailabilitySummary"),
functionBody(app, "failureMessage")
].join("\n");
return (
/codeAgentAvailability/u.test(app) &&
/codeAgentAvailabilityFrom/u.test(app) &&
/sanitizeCodeAgentAvailability/u.test(app) &&
/latestCompletedAgentMessage/u.test(app) &&
blockedCodeAgentUiLabels.some((label) => app.includes(label)) &&
/provider_unavailable/u.test(app) &&
@@ -1277,6 +1339,7 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
!/status === "failed" \? "dev-live"/u.test(app) &&
!/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) &&
!/provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu.test(source) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef/u.test(userFacingBodies) &&
!/sk-[A-Za-z0-9._-]{8,}/u.test(source)
);
}
+34 -13
View File
@@ -1320,7 +1320,7 @@ function renderWiringList(summary = null) {
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
const traceEvidence = m3Live
? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ")
: "m3-route-required / waiting-for-dev-live";
: "等待可信记录";
const tr = document.createElement("tr");
tr.append(cell(M3_TRUSTED_ROUTE.fromResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.fromPort, "mono"));
@@ -1330,7 +1330,7 @@ function renderWiringList(summary = null) {
const status = document.createElement("td");
status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked"));
tr.append(status);
tr.append(cell(m3Live ? "DEV-LIVE" : "SOURCE / required M3 route", "wrap"));
tr.append(cell(m3Live ? "DEV-LIVE" : "来源拓扑", "wrap"));
tr.append(cell(traceEvidence, "mono wrap"));
replaceChildren(el.wiringBody, tr);
@@ -1383,7 +1383,7 @@ function codeAgentRecordCards() {
recordField("conversation", state.conversationId),
recordField("trace", message.traceId),
recordField("message", message.messageId),
message.error?.message ? `失败原因=${message.error.message}` : null
message.error?.message ? `失败原因=${safeFailureReason(message.error.message)}` : null
].filter(Boolean).join(" / "),
tone: message.status === "failed" ? "blocked" : "source"
})
@@ -1572,6 +1572,15 @@ function recordField(label, value) {
return `${label}=${value}`;
}
function safeFailureReason(value) {
const text = String(value ?? "").trim();
if (!text) return "未知错误";
if (/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef/iu.test(text)) {
return "服务暂不可用";
}
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
}
function sourceKindLabel(tone) {
return statusLabel(tone);
}
@@ -1703,7 +1712,7 @@ function renderAgentChatStatus(status, result = null) {
completed: "DEV-LIVE 回复",
source: "SOURCE 回复",
failed: "发送失败",
blocked: "BLOCKED 凭证缺口"
blocked: "服务受阻"
};
el.agentChatStatus.textContent = labels[status] ?? statusLabel(status);
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`;
@@ -1755,10 +1764,7 @@ function failureMessage(result) {
const availability = result.error?.providerStatus || result.availability?.status !== "blocked"
? codeAgentAvailabilityFromResult(result)
: result.availability ?? codeAgentAvailabilityFromResult(result);
const missing = Array.isArray(result.error?.missingEnv) && result.error.missingEnv.length > 0
? ` 当前缺口:${result.error.missingEnv.join("、")}`
: "";
return `${availability.summary}${missing}`;
return codeAgentBlockedSummary(availability);
}
const missing = [
...(result.error?.missingCommands ?? []),
@@ -1947,7 +1953,8 @@ function isHttpNon2xxStatus(value) {
}
function codeAgentAvailabilityFrom(live) {
return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
const availability = live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
return sanitizeCodeAgentAvailability(availability);
}
function codeAgentAvailabilityFromResult(result) {
@@ -1973,11 +1980,21 @@ function codeAgentAvailabilityFromResult(result) {
};
}
function sanitizeCodeAgentAvailability(availability) {
if (!availability || typeof availability !== "object") return availability;
if (availability.status !== "blocked") return availability;
return {
...availability,
blocker: "服务受阻",
summary: codeAgentBlockedSummary(availability)
};
}
function codeAgentStatusMessage(availability) {
if (availability?.status === "blocked") {
return {
role: "system",
title: "Code Agent 状态:BLOCKED 凭证缺口",
title: "Code Agent 状态:服务受阻",
text: codeAgentBlockedSummary(availability),
status: "blocked"
};
@@ -2008,7 +2025,7 @@ function codeAgentStatusMessage(availability) {
function codeAgentPromptText(availability) {
if (availability?.status === "blocked") {
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat,但当前 DEV provider 凭证缺口会返回结构化失败,不会冒充真实 Codex 回复。";
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
}
return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。";
}
@@ -2029,7 +2046,11 @@ function codeAgentControlSummary(availability) {
}
function codeAgentBlockedSummary(availability) {
return availability?.summary ?? "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。";
const providerStatus = availability?.error?.providerStatus ?? availability?.providerStatus;
if (providerStatus) {
return `真实后端已接入,但当前上游响应 HTTP ${providerStatus};工作台保持只读和可重试,不会冒充真实 Code Agent 回复。`;
}
return "真实后端已接入,但当前 Code Agent 服务暂不可用;工作台保持只读和可重试,不会冒充真实回复。";
}
function untrustedCompletionMessage(result) {
@@ -2039,7 +2060,7 @@ function untrustedCompletionMessage(result) {
function codeAgentAvailabilitySummary() {
if (state.codeAgentAvailability?.status === "blocked") {
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
}
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。";
+4 -4
View File
@@ -230,7 +230,7 @@
<span class="hardware-row-label">hwlab-patch-panel</span>
<span class="state-tag tone-source">SOURCE</span>
</div>
<span class="hardware-row-value">SOURCE route</span>
<span class="hardware-row-value">来源拓扑</span>
<span class="hardware-row-detail">res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 仅为源码兜底显示。</span>
</div>
<div class="hardware-row tone-border-blocked" data-source-kind="BLOCKED">
@@ -238,7 +238,7 @@
<span class="hardware-row-label">DO1 -> patch-panel -> DI1</span>
<span class="state-tag tone-blocked">BLOCKED</span>
</div>
<span class="hardware-row-value">waiting-for-dev-live</span>
<span class="hardware-row-value">等待可信记录</span>
<span class="hardware-row-detail">缺少 patch-panel live report 与 operation/trace/audit/evidence IDs,不能标为 DEV-LIVE。</span>
</div>
</div>
@@ -321,8 +321,8 @@
<td class="mono wrap">res_boxsimu_2</td>
<td class="mono">DI1</td>
<td><span class="state-tag tone-blocked">阻塞 BLOCKED</span></td>
<td class="wrap">SOURCE / required M3 route</td>
<td class="mono wrap">m3-route-required / waiting-for-dev-live</td>
<td class="wrap">来源拓扑 / required M3 route</td>
<td class="mono wrap">等待可信记录(m3-route-required / waiting-for-dev-live</td>
</tr>
</tbody>
</table>
+21 -3
View File
@@ -169,6 +169,7 @@ assert.match(app, /auditCard\(event, liveRecordTone\(event, live\), liveRecordDu
assert.match(app, /evidenceCard\(record, liveRecordTone\(record, live\), liveRecordDurabilityBlocker\(live\)\)/);
assert.match(app, /function recordGroup/);
assert.match(app, /function recordField/);
assert.match(app, /function safeFailureReason/);
assert.match(app, /function sourceKindLabel/);
assert.match(app, /function liveFailureCard/);
assert.match(app, /state\.liveSurface/);
@@ -274,7 +275,7 @@ for (const helpTerm of [
}
const workspaceHtml = sectionSource(html, "workspace");
const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`;
for (const backendTerm of [/Gate/u, /诊断/u, /验收/u, /BLOCKED/u, /M0-M5/u, /执行轨迹/u]) {
for (const backendTerm of [/Gate/u, /诊断/u, /验收/u, /BLOCKED/u, /M0-M5/u, /执行轨迹/u, /OPENAI_API_KEY/u, /Secret/u, /secretRef/u, /hwlab-code-agent-provider/u, /m3-route-required/u, /waiting-for-dev-live/u]) {
assert.doesNotMatch(visibleShellHtml, backendTerm, `default workbench must not expose backend term ${backendTerm}`);
}
for (const fullNavLabel of ["工作台", "内部复核", "使用说明", "资源树"]) {
@@ -355,11 +356,11 @@ assert.match(app, /isHttpNon2xxStatus/);
assert.match(app, /providerStatus/);
assert.match(app, /codeAgentAvailability/);
assert.match(app, /codeAgentAvailabilityFrom/);
assert.match(app, /sanitizeCodeAgentAvailability/);
assert.match(app, /latestCompletedAgentMessage/);
assert.match(app, /BLOCKED 凭证缺口/);
assert.match(app, /服务受阻/);
assert.match(app, /provider_unavailable/);
assert.match(app, /OPENAI_API_KEY/);
assert.match(app, /hwlab-code-agent-provider\/openai-api-key/);
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
assert.match(app, /TRUSTED_CODE_AGENT_PROVIDERS/);
@@ -400,6 +401,23 @@ assert.match(styles, /\.message-user\s*{/);
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
assert.match(styles, /\.message-evidence\s*{/);
assert.match(styles, /\.message-evidence-chip\s*{/);
assert.match(styles, /\.rail-button\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/s);
assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/s);
assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s);
assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-evidence\s*{[\s\S]*?grid-column:\s*1;/);
for (const userFacingFunctionName of [
"codeAgentStatusMessage",
"codeAgentPromptText",
"codeAgentControlSummary",
"codeAgentBlockedSummary",
"codeAgentAvailabilitySummary",
"failureMessage",
"renderWiringList"
]) {
assert.doesNotMatch(functionBody(app, userFacingFunctionName), /OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u, `${userFacingFunctionName} must keep default copy sanitized`);
}
assert.match(app, /state\.conversationId/);
assert.match(app, /conversationId/);
assert.match(app, /messageId/);
+23 -1
View File
@@ -103,6 +103,7 @@ ul {
.rail-button {
width: 100%;
min-width: 0;
min-height: 36px;
padding: 6px 5px;
border: 1px solid transparent;
@@ -112,7 +113,9 @@ ul {
font-weight: 760;
line-height: 1.2;
cursor: pointer;
text-align: center;
overflow-wrap: anywhere;
word-break: keep-all;
}
.rail-button:hover,
@@ -202,6 +205,8 @@ h3 {
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 1 auto;
width: fit-content;
max-width: 100%;
min-height: 22px;
@@ -212,6 +217,9 @@ h3 {
font-size: 10px;
font-weight: 760;
text-transform: uppercase;
line-height: 1.25;
white-space: normal;
text-align: center;
overflow-wrap: anywhere;
}
@@ -383,6 +391,7 @@ h3 {
}
.probe-card {
min-width: 0;
display: grid;
align-content: center;
gap: 4px;
@@ -393,8 +402,9 @@ h3 {
.probe-card strong {
font-size: 17px;
line-height: 1;
line-height: 1.2;
text-transform: uppercase;
overflow-wrap: anywhere;
}
.probe-card small {
@@ -875,6 +885,7 @@ h3 {
}
.side-tab {
min-width: 0;
min-height: 34px;
padding: 6px 4px;
border: 0;
@@ -884,6 +895,8 @@ h3 {
font-size: 11px;
font-weight: 760;
cursor: pointer;
overflow-wrap: anywhere;
word-break: keep-all;
}
.side-tab:last-child {
@@ -1254,6 +1267,7 @@ tbody tr:last-child td {
.rail-button {
min-height: 42px;
line-height: 1.15;
}
.explorer {
@@ -1296,6 +1310,10 @@ tbody tr:last-child td {
padding: 8px 10px;
}
.probe-card strong {
font-size: 15px;
}
.workbench-view,
.gate-view,
.help-view,
@@ -1341,6 +1359,10 @@ tbody tr:last-child td {
.trace-item {
grid-template-columns: 1fr;
}
.message-evidence {
grid-column: 1;
}
}
@media (max-width: 520px) {