diff --git a/package.json b/package.json
index 50941235..c519c546 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs",
- "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
+ "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs
index a331e6ee..eb68c68f 100644
--- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs
+++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs
@@ -73,6 +73,8 @@ const requiredWebAssets = Object.freeze([
"index.html",
"styles.css",
"app.mjs",
+ "code-agent-facts.mjs",
+ "code-agent-status.mjs",
"live-status.mjs",
"runtime.mjs",
"gate-summary.mjs",
@@ -83,7 +85,7 @@ const requiredWebAssets = Object.freeze([
"third_party/marked/LICENSE"
]);
-const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "live-status.mjs", "wiring-status.mjs"]);
+const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", "live-status.mjs", "wiring-status.mjs"]);
const chineseWorkbenchLabels = Object.freeze([
"HWLAB 云工作台",
@@ -534,6 +536,18 @@ function runStaticSmoke() {
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
});
+ addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for fallback, one-shot, read-only session, skill CLI control, blockers, trace, and current deployed revision.", {
+ blocker: "observability_blocker",
+ evidence: [
+ "code-agent-summary",
+ "fallback-text-chat-only",
+ "stateless-one-shot",
+ "read-only-session-tools",
+ "skill-cli-api-control",
+ "当前部署 revision"
+ ]
+ });
+
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
@@ -1632,6 +1646,8 @@ function readStaticFiles() {
html: readText("web/hwlab-cloud-web/index.html"),
styles: readText("web/hwlab-cloud-web/styles.css"),
app: readText("web/hwlab-cloud-web/app.mjs"),
+ codeAgentFacts: readText("web/hwlab-cloud-web/code-agent-facts.mjs"),
+ codeAgentStatus: readText("web/hwlab-cloud-web/code-agent-status.mjs"),
liveStatus: readText("web/hwlab-cloud-web/live-status.mjs"),
wiringStatus: readText("web/hwlab-cloud-web/wiring-status.mjs"),
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
@@ -2464,6 +2480,32 @@ function hasCodeAgentLongTimeoutContract(app) {
);
}
+function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus }) {
+ return (
+ /id=["']code-agent-summary["']/u.test(html) &&
+ /id=["']code-agent-summary-label["']/u.test(html) &&
+ /id=["']code-agent-summary-capability["']/u.test(html) &&
+ /id=["']code-agent-summary-trace["']/u.test(html) &&
+ /function\s+renderCodeAgentSummary\s*\(/u.test(app) &&
+ /classifyCodeAgentStatusSummary/u.test(app) &&
+ /currentCodeAgentStatusSummary/u.test(app) &&
+ /codeAgent\.status/u.test(app) &&
+ /当前部署 revision/u.test(app) &&
+ /fallback-text-chat-only/u.test(codeAgentStatus) &&
+ /stateless-one-shot/u.test(codeAgentStatus) &&
+ /read-only-session-tools/u.test(codeAgentStatus) &&
+ /skill-cli-api-control/u.test(codeAgentStatus) &&
+ /currentDeploymentRevision/u.test(codeAgentStatus) &&
+ /unsafeGreenForNonReady/u.test(codeAgentStatus) &&
+ /provider_config_blocked/u.test(codeAgentStatus) &&
+ !/sourceMain|SOURCE main|待部署 commit|targetCommit/u.test(codeAgentStatus) &&
+ /\.code-agent-summary\s*\{/u.test(styles) &&
+ /\.tone-border-warn/u.test(styles) &&
+ /\.tone-border-blocked/u.test(styles) &&
+ /\.tone-border-ok/u.test(styles)
+ );
+}
+
function hasCodeAgentConversationUxStates({ app, styles }) {
const submitBody = functionBody(app, "initCommandBar");
return (
diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs
index 4c380b3a..03aceae6 100644
--- a/web/hwlab-cloud-web/app.mjs
+++ b/web/hwlab-cloud-web/app.mjs
@@ -6,6 +6,7 @@ import {
skillsSummary as compactSkillsSummary,
toolCallsSummary as compactToolCallsSummary
} from "./code-agent-facts.mjs";
+import { classifyCodeAgentStatusSummary, codeAgentAvailabilityFromLive } from "./code-agent-status.mjs";
import { classifyWorkbenchLiveStatus } from "./live-status.mjs";
import { marked } from "./third_party/marked/marked.esm.js";
import { wiringPresentationFromSummary } from "./wiring-status.mjs";
@@ -96,6 +97,12 @@ const el = {
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
agentChatStatus: byId("agent-chat-status"),
+ codeAgentSummary: byId("code-agent-summary"),
+ codeAgentSummaryIcon: byId("code-agent-summary-icon"),
+ codeAgentSummaryLabel: byId("code-agent-summary-label"),
+ codeAgentSummaryCapability: byId("code-agent-summary-capability"),
+ codeAgentSummaryTrace: byId("code-agent-summary-trace"),
+ codeAgentSummaryDetail: byId("code-agent-summary-detail"),
conversationList: byId("conversation-list"),
taskList: byId("task-list"),
gateSourceStatus: byId("gate-source-status"),
@@ -167,6 +174,7 @@ initM3Control();
initGateControls();
renderStaticWorkbench();
renderProbePending();
+renderCodeAgentSummary();
loadHelpSurface();
loadLiveSurface().then(renderLiveSurface);
@@ -638,6 +646,7 @@ function initCommandBar() {
state.chatPending = true;
el.commandInput.value = "";
renderAgentChatStatus("running");
+ renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
@@ -696,6 +705,7 @@ function initCommandBar() {
el.commandInput.value = value;
}
renderAgentChatStatus(status, result);
+ renderCodeAgentSummary();
} catch (error) {
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const presentation = agentFailurePresentation(error, { traceId });
@@ -715,8 +725,10 @@ function initCommandBar() {
};
el.commandInput.value = value;
renderAgentChatStatus("failed", state.chatMessages[index]);
+ renderCodeAgentSummary();
} finally {
state.chatPending = false;
+ renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
@@ -730,6 +742,7 @@ function initCommandBar() {
state.chatPending = false;
el.commandInput.value = "";
renderAgentChatStatus("idle");
+ renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
@@ -769,12 +782,14 @@ function renderStaticWorkbench() {
renderWiringList(null);
renderRecords(null);
renderGateTable();
+ renderCodeAgentSummary();
}
function renderProbePending() {
el.liveStatus.textContent = "等待验证";
el.liveStatus.className = "tone-pending";
el.liveDetail.textContent = "正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。";
+ renderCodeAgentSummary();
}
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc")) {
@@ -1067,6 +1082,7 @@ function renderLiveSurface(live) {
el.liveDetail.textContent = surfaceStatus.detail ?? codeAgentAvailabilitySummary();
renderAgentChatStatus(deriveAgentChatStatus());
+ renderCodeAgentSummary();
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status ?? runtimeSummary);
renderRecords(live);
@@ -2216,6 +2232,53 @@ function renderAgentChatStatus(status, result = null) {
}
}
+function renderCodeAgentSummary() {
+ const summary = currentCodeAgentStatusSummary();
+ el.codeAgentSummary.className = `code-agent-summary tone-border-${toneClass(summary.tone)}`;
+ el.codeAgentSummary.dataset.codeAgentStatusKind = summary.kind;
+ el.codeAgentSummaryIcon.textContent = summary.icon;
+ el.codeAgentSummaryIcon.className = `code-agent-summary-icon tone-${toneClass(summary.tone)}`;
+ el.codeAgentSummaryLabel.textContent = summary.label;
+ el.codeAgentSummaryLabel.className = `code-agent-summary-label tone-${toneClass(summary.tone)}`;
+ el.codeAgentSummaryCapability.textContent = summary.capabilityLevel;
+ el.codeAgentSummaryTrace.textContent = `trace ${summary.lastTraceId}`;
+ replaceChildren(el.codeAgentSummaryDetail, ...codeAgentSummaryRows(summary));
+}
+
+function currentCodeAgentStatusSummary() {
+ return classifyCodeAgentStatusSummary({
+ availability: state.codeAgentAvailability,
+ latestMessage: latestChatResult(),
+ live: state.liveSurface
+ });
+}
+
+function codeAgentSummaryRows(summary) {
+ return [
+ codeAgentSummaryRow("codeAgent.status", summary.codeAgentStatus, summary.tone),
+ codeAgentSummaryRow("provider/mode/backend", summary.providerModeBackend, "source"),
+ codeAgentSummaryRow("capabilityLevel", summary.capabilityLevel, summary.tone),
+ codeAgentSummaryRow("session", summary.sessionSummary, sessionSummaryTone(summary)),
+ codeAgentSummaryRow("readiness blockers", summary.blockersLabel, summary.readinessBlockers.length > 0 ? "blocked" : "source"),
+ codeAgentSummaryRow("last traceId", summary.lastTraceId, "source"),
+ codeAgentSummaryRow("当前部署 revision", summary.deploymentRevision, "source")
+ ];
+}
+
+function codeAgentSummaryRow(label, value, tone) {
+ const row = document.createElement("div");
+ row.className = "code-agent-summary-row";
+ row.append(textSpan(label, "code-agent-summary-key"), textSpan(value, `code-agent-summary-value tone-${toneClass(tone)}`));
+ return row;
+}
+
+function sessionSummaryTone(summary) {
+ if (summary.kind === "long-lived-session") return "ok";
+ if (summary.kind === "blocked" || summary.kind === "skill-cli-api-blocked") return "blocked";
+ if (["read-only-session-tools", "stateless-one-shot", "fallback-text-chat-only", "degraded"].includes(summary.kind)) return "warn";
+ return "source";
+}
+
function agentStatusLabel(status, result, labels) {
if (status !== "failed") return labels[status] ?? statusLabel(status);
const presentation = agentFailurePresentation(result?.error, { result });
@@ -2683,8 +2746,7 @@ function isHttpNon2xxStatus(value) {
}
function codeAgentAvailabilityFrom(live) {
- const availability = live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
- return sanitizeCodeAgentAvailability(availability);
+ return sanitizeCodeAgentAvailability(codeAgentAvailabilityFromLive(live));
}
function codeAgentAvailabilityFromResult(result) {
diff --git a/web/hwlab-cloud-web/code-agent-status.mjs b/web/hwlab-cloud-web/code-agent-status.mjs
new file mode 100644
index 00000000..a81db190
--- /dev/null
+++ b/web/hwlab-cloud-web/code-agent-status.mjs
@@ -0,0 +1,313 @@
+const GREEN_TONES = new Set(["ok", "available", "ready", "dev-live", "live", "pass"]);
+const SECRET_MARKER = /\b(?:OPENAI_API_KEY|DATABASE_URL|secret|secretref|token|password|passwd|credential|private key|kubeconfig|密钥|密码|凭证)\b/iu;
+const NON_LIVE_VALUES = new Set(["", "n/a", "none", "null", "undefined", "unknown", "not_observed", "not-observed"]);
+const MAX_FIELD_LENGTH = 96;
+
+export function classifyCodeAgentStatusSummary({ availability = null, latestMessage = null, live = null } = {}) {
+ const observedAvailability = availability ?? codeAgentAvailabilityFromLive(live);
+ const payload = safeMerge(observedAvailability, latestMessage);
+ const blockers = readinessBlockers(payload);
+ const classification = classifyPayload(payload, blockers);
+ const safeTone = unsafeGreenForNonReady(classification, blockers) ? "warn" : classification.tone;
+ const provider = safeField(firstString(payload.provider, observedAvailability?.provider));
+ const mode = safeField(firstString(payload.mode, payload.providerTrace?.mode, observedAvailability?.mode));
+ const backend = safeField(firstString(payload.backend, observedAvailability?.backend));
+ const codeAgentStatus = safeField(firstString(payload.status, payload.readinessStatus, observedAvailability?.status));
+ const capabilityLevel = safeField(firstString(payload.capabilityLevel, payload.runner?.capabilityLevel, observedAvailability?.capabilityLevel));
+ const sessionMode = safeField(firstString(payload.sessionMode, payload.session?.sessionMode, payload.runnerTrace?.sessionMode, payload.runner?.sessionMode));
+ const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status));
+ const lastTraceId = safeTraceId(firstString(
+ latestMessage?.traceId,
+ payload.traceId,
+ payload.session?.lastTraceId,
+ payload.runnerTrace?.lastTraceId,
+ payload.runnerTrace?.traceId,
+ payload.providerTrace?.traceId,
+ observedAvailability?.traceId
+ ));
+ const deploymentRevision = currentDeploymentRevision(live);
+
+ return {
+ ...classification,
+ tone: safeTone,
+ provider,
+ mode,
+ backend,
+ providerModeBackend: compactJoin([provider, mode, backend], " / "),
+ codeAgentStatus,
+ capabilityLevel,
+ sessionMode,
+ sessionStatus,
+ sessionSummary: compactJoin([sessionMode, sessionStatus], " / "),
+ readinessBlockers: blockers,
+ blockersLabel: blockers.length > 0 ? blockers.join(", ") : "无",
+ lastTraceId: lastTraceId || "未观测",
+ deploymentRevision,
+ fields: {
+ provider,
+ mode,
+ backend,
+ codeAgentStatus,
+ capabilityLevel,
+ sessionMode,
+ sessionStatus,
+ readinessBlockers: blockers,
+ lastTraceId: lastTraceId || null,
+ deploymentRevision
+ }
+ };
+}
+
+export function codeAgentAvailabilityFromLive(live = {}) {
+ return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? live?.health?.data?.codeAgent ?? null;
+}
+
+export function currentDeploymentRevision(live = {}) {
+ const payloads = [
+ live?.healthLive?.data,
+ live?.restIndex?.data,
+ live?.health?.data,
+ live?.adapter?.data
+ ].filter(Boolean);
+ for (const payload of payloads) {
+ const revision = firstString(
+ payload?.commit?.id,
+ payload?.commitId,
+ payload?.revision,
+ payload?.runtime?.commitId,
+ payload?.image?.tag,
+ payload?.imageTag,
+ payload?.runtime?.imageTag
+ );
+ if (revision && !NON_LIVE_VALUES.has(revision.toLowerCase())) {
+ return shortRevision(revision);
+ }
+ }
+ return "未观测";
+}
+
+function classifyPayload(payload, blockers) {
+ if (!payload || Object.keys(payload).length === 0) {
+ return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
+ }
+
+ if (isSkillCliApiControl(payload)) {
+ const ready = isReadySkillCli(payload);
+ return ready
+ ? status("skill-cli-api-control", "HWLAB Skill CLI 控制", "ok", "◆", "通过 HWLAB API 受控路径执行指定能力。")
+ : status("skill-cli-api-blocked", "Skill CLI 受阻", "blocked", "×", "HWLAB Skill CLI 控制路径当前受阻。");
+ }
+
+ if (isLongLivedReady(payload)) {
+ return status("long-lived-session", "长会话可用", "ok", "●", "repo-owned 长会话通道可复用。");
+ }
+
+ if (isReadOnlySessionTools(payload)) {
+ return status("read-only-session-tools", "只读会话工具", "warn", "◇", "只读 runner 工具可用,不能写工作区或冒充完整长会话。");
+ }
+
+ if (isStatelessOneShot(payload)) {
+ return status("stateless-one-shot", "一次性执行", "warn", "△", "当前回复来自一次性执行,不是可复用长会话。");
+ }
+
+ if (isBlocked(payload)) {
+ return status("blocked", "服务受阻", "blocked", "×", "Code Agent 通道暂不可用或 readiness 未通过。");
+ }
+
+ if (isFallbackTextChatOnly(payload)) {
+ return status("fallback-text-chat-only", "文本备用通道", "warn", "◌", "只有文本对话能力,没有 workspace tools 或 session 控制。");
+ }
+
+ if (isDegraded(payload, blockers)) {
+ return status("degraded", "降级可用", "warn", "!", "Code Agent 有部分能力,但 readiness 仍有阻塞项。");
+ }
+
+ if (payload.status === "available" || payload.ready === true || payload.status === "completed") {
+ return status("available", "通道可用", "ok", "●", "受控对话通道已返回可用状态。");
+ }
+
+ return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
+}
+
+function status(kind, label, tone, icon, summary) {
+ return { kind, label, tone, icon, summary };
+}
+
+function safeMerge(availability, latestMessage) {
+ const base = availability && typeof availability === "object" ? availability : {};
+ const message = latestMessage && typeof latestMessage === "object" ? latestMessage : {};
+ const merged = {
+ ...base,
+ ...message,
+ runner: { ...(base.runner ?? {}), ...(message.runner ?? {}) },
+ session: mergeSession(base.session, message.session),
+ runnerTrace: { ...(base.runnerTrace ?? {}), ...(message.runnerTrace ?? {}) },
+ providerTrace: { ...(base.providerTrace ?? {}), ...(message.providerTrace ?? {}) },
+ longLivedSessionGate: { ...(base.longLivedSessionGate ?? {}), ...(message.longLivedSessionGate ?? {}) },
+ codexStdioFeasibility: { ...(base.codexStdioFeasibility ?? base.codexStdio ?? {}), ...(message.codexStdioFeasibility ?? {}) }
+ };
+ if (base.blockers || message.blockers) merged.blockers = [...(base.blockers ?? []), ...(message.blockers ?? [])];
+ if (base.blockerCodes || message.blockerCodes) merged.blockerCodes = [...(base.blockerCodes ?? []), ...(message.blockerCodes ?? [])];
+ if (base.runnerLimitations || message.runnerLimitations) merged.runnerLimitations = [...(base.runnerLimitations ?? []), ...(message.runnerLimitations ?? [])];
+ if (message.toolCalls !== undefined) merged.toolCalls = message.toolCalls;
+ if (message.skills !== undefined) merged.skills = message.skills;
+ return merged;
+}
+
+function mergeSession(baseSession, messageSession) {
+ const base = typeof baseSession === "string" ? { status: baseSession } : (baseSession ?? {});
+ const message = typeof messageSession === "string" ? { status: messageSession } : (messageSession ?? {});
+ return { ...base, ...message };
+}
+
+function isSkillCliApiControl(payload) {
+ return (
+ payload.provider === "hwlab-skill-cli" ||
+ payload.runner?.kind === "hwlab-m3-io-skill-cli" ||
+ payload.sessionMode === "controlled-m3-io-skill-cli" ||
+ String(payload.capabilityLevel ?? "").startsWith("hwlab-api-control") ||
+ array(payload.toolCalls).some((tool) => tool?.type === "skill-cli" || tool?.route === "/v1/m3/io")
+ );
+}
+
+function isReadySkillCli(payload) {
+ return (
+ payload.capabilityLevel === "hwlab-api-control-ready" ||
+ payload.providerTrace?.controlReady === true ||
+ array(payload.toolCalls).some((tool) => tool?.controlReady === true && tool?.status === "completed")
+ ) && !hasBlockingSkillCliSignal(payload);
+}
+
+function hasBlockingSkillCliSignal(payload) {
+ return payload.capabilityLevel === "hwlab-api-control-blocked" ||
+ payload.providerTrace?.controlReady === false ||
+ array(payload.toolCalls).some((tool) => tool?.status === "blocked" || tool?.controlReady === false);
+}
+
+function isLongLivedReady(payload) {
+ return (
+ payload.longLivedSessionGate?.status === "pass" ||
+ payload.longLivedSessionGate?.pass === true ||
+ payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true ||
+ payload.capabilityLevel === "long-lived-codex-stdio-session" ||
+ payload.sessionMode === "codex-mcp-stdio-long-lived"
+ ) && payload.status !== "failed" && payload.status !== "blocked" && payload.ready !== false;
+}
+
+function isReadOnlySessionTools(payload) {
+ return (
+ payload.capabilityLevel === "read-only-session-tools" ||
+ payload.capabilityLevel === "read-only-tools" ||
+ payload.agentKind === "controlled-readonly-session-registry" ||
+ payload.sessionMode === "controlled-readonly-session-registry" ||
+ payload.implementationType === "controlled-readonly-session-registry" ||
+ payload.runner?.kind === "hwlab-readonly-runner" ||
+ (payload.runner?.writeCapable === false && payload.capabilityLevel !== "text-chat-only" && payload.runner?.kind !== "openai-responses-fallback") ||
+ payload.partialReady === true
+ );
+}
+
+function isStatelessOneShot(payload) {
+ return (
+ payload.sessionMode === "ephemeral-one-shot" ||
+ payload.runner?.kind === "codex-cli-one-shot-ephemeral" ||
+ payload.implementationType === "codex-cli-one-shot-ephemeral" ||
+ array(payload.runnerLimitations).includes("one-shot")
+ );
+}
+
+function isFallbackTextChatOnly(payload) {
+ return (
+ payload.capabilityLevel === "text-chat-only" ||
+ payload.runner?.kind === "openai-responses-fallback" ||
+ payload.implementationType === "openai-responses-fallback" ||
+ payload.agentKind === "openai-fallback" ||
+ payload.provider === "openai-responses"
+ );
+}
+
+function isBlocked(payload) {
+ return payload.status === "blocked" ||
+ payload.status === "failed" ||
+ payload.ready === false ||
+ payload.capabilityLevel === "blocked" ||
+ Boolean(payload.error);
+}
+
+function isDegraded(payload, blockers) {
+ return payload.status === "partial" ||
+ payload.status === "degraded" ||
+ payload.readinessStatus === "partial" ||
+ payload.capabilityStatus === "controlled-readonly-session-registry" ||
+ blockers.length > 0;
+}
+
+function readinessBlockers(payload) {
+ const values = [
+ ...array(payload?.blockerCodes),
+ ...array(payload?.blockers).map((item) => item?.code ?? item),
+ ...array(payload?.longLivedSessionGate?.blockers).map((item) => item?.code ?? item),
+ ...array(payload?.codexStdioFeasibility?.blockers).map((item) => item?.code ?? item),
+ payload?.reason,
+ payload?.error?.code,
+ payload?.providerStatus ? `provider_http_${payload.providerStatus}` : null,
+ payload?.error?.providerStatus ? `provider_http_${payload.error.providerStatus}` : null,
+ ...array(payload?.missingEnv).map(() => "provider_config_blocked")
+ ];
+ return [...new Set(values.map(safeBlocker).filter(Boolean))].slice(0, 4);
+}
+
+function safeBlocker(value) {
+ if (value && typeof value === "object" && !Array.isArray(value) && !value.code && !value.reason && !value.message) return null;
+ const text = safeField(typeof value === "object" ? value?.code ?? value?.reason ?? value?.message : value);
+ if (!text || text === "未观测" || /^_+$/u.test(text)) return null;
+ if (SECRET_MARKER.test(text)) return "provider_config_blocked";
+ return text.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 72);
+}
+
+function unsafeGreenForNonReady(classification, blockers) {
+ return GREEN_TONES.has(classification.tone) &&
+ (classification.kind.includes("blocked") || classification.kind === "degraded" || blockers.some((blocker) => /blocked|unavailable|failed|degraded/iu.test(blocker)));
+}
+
+function currentString(value) {
+ const text = String(value ?? "").trim();
+ if (!text || NON_LIVE_VALUES.has(text.toLowerCase())) return null;
+ return text;
+}
+
+function firstString(...values) {
+ for (const value of values) {
+ const text = currentString(value);
+ if (text) return text;
+ }
+ return null;
+}
+
+function safeField(value) {
+ const text = currentString(value);
+ if (!text) return "未观测";
+ if (SECRET_MARKER.test(text)) return "已隐藏";
+ return text.replace(/\s+/gu, " ").slice(0, MAX_FIELD_LENGTH);
+}
+
+function safeTraceId(value) {
+ const text = currentString(value);
+ if (!text || SECRET_MARKER.test(text)) return null;
+ return text.replace(/\s+/gu, "").slice(0, MAX_FIELD_LENGTH);
+}
+
+function shortRevision(value) {
+ const text = safeTraceId(value);
+ if (!text) return "未观测";
+ return /^[a-f0-9]{12,40}$/iu.test(text) ? text.slice(0, 12) : text.slice(0, 24);
+}
+
+function compactJoin(values, separator) {
+ const parts = values.map((value) => value === "未观测" ? null : value).filter(Boolean);
+ return parts.length > 0 ? parts.join(separator) : "未观测";
+}
+
+function array(value) {
+ return Array.isArray(value) ? value : [];
+}
diff --git a/web/hwlab-cloud-web/code-agent-status.test.mjs b/web/hwlab-cloud-web/code-agent-status.test.mjs
new file mode 100644
index 00000000..b2c9e4b4
--- /dev/null
+++ b/web/hwlab-cloud-web/code-agent-status.test.mjs
@@ -0,0 +1,189 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { classifyCodeAgentStatusSummary } from "./code-agent-status.mjs";
+
+const liveRevision = Object.freeze({
+ healthLive: {
+ ok: true,
+ status: 200,
+ data: {
+ serviceId: "hwlab-cloud-api",
+ status: "ok",
+ commitId: "7de6edd2c41f54e4265e822d2593fa67057fa41a"
+ }
+ }
+});
+
+test("summarizes long-lived Code Agent as green with current deployment revision only", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ live: liveRevision,
+ availability: {
+ status: "codex-stdio-feasible",
+ ready: true,
+ provider: "codex-stdio",
+ mode: "codex-stdio",
+ backend: "hwlab-cloud-api/codex-stdio",
+ capabilityLevel: "long-lived-codex-stdio-session",
+ sessionMode: "codex-mcp-stdio-long-lived",
+ session: {
+ status: "idle",
+ lastTraceId: "trc_ready"
+ },
+ longLivedSessionGate: {
+ status: "pass"
+ },
+ codexStdioFeasibility: {
+ canStartLongLivedCodexStdio: true
+ }
+ }
+ });
+
+ assert.equal(summary.kind, "long-lived-session");
+ assert.equal(summary.label, "长会话可用");
+ assert.equal(summary.tone, "ok");
+ assert.equal(summary.codeAgentStatus, "codex-stdio-feasible");
+ assert.equal(summary.providerModeBackend, "codex-stdio / codex-stdio / hwlab-cloud-api/codex-stdio");
+ assert.equal(summary.capabilityLevel, "long-lived-codex-stdio-session");
+ assert.equal(summary.sessionSummary, "codex-mcp-stdio-long-lived / idle");
+ assert.equal(summary.readinessBlockers.length, 0);
+ assert.equal(summary.lastTraceId, "trc_ready");
+ assert.equal(summary.deploymentRevision, "7de6edd2c41f");
+ assert.equal(Object.keys(summary.fields).includes("sourceMain"), false);
+});
+
+test("maps degraded readonly session tools to warning, not green", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ availability: {
+ status: "partial",
+ ready: false,
+ provider: "codex-readonly-runner",
+ backend: "hwlab-cloud-api/controlled-readonly-session-registry",
+ capabilityLevel: "read-only-session-tools",
+ sessionMode: "controlled-readonly-session-registry",
+ session: {
+ status: "idle",
+ lastTraceId: "trc_readonly"
+ },
+ runner: {
+ kind: "hwlab-readonly-runner",
+ writeCapable: false
+ },
+ blockers: [
+ { code: "stdio_protocol_not_wired" }
+ ]
+ }
+ });
+
+ assert.equal(summary.kind, "read-only-session-tools");
+ assert.equal(summary.label, "只读会话工具");
+ assert.equal(summary.tone, "warn");
+ assert.equal(summary.readinessBlockers.includes("stdio_protocol_not_wired"), true);
+ assert.equal(summary.lastTraceId, "trc_readonly");
+});
+
+test("maps text fallback to warning with safe labels", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ availability: {
+ status: "available",
+ ready: true,
+ provider: "openai-responses",
+ mode: "openai",
+ backend: "hwlab-cloud-api/openai-responses",
+ capabilityLevel: "text-chat-only",
+ runner: {
+ kind: "openai-responses-fallback"
+ }
+ }
+ });
+
+ assert.equal(summary.kind, "fallback-text-chat-only");
+ assert.equal(summary.label, "文本备用通道");
+ assert.equal(summary.tone, "warn");
+ assert.equal(summary.provider, "openai-responses");
+ assert.equal(summary.capabilityLevel, "text-chat-only");
+});
+
+test("maps stateless one-shot runner separately from long-lived sessions", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ latestMessage: {
+ status: "completed",
+ provider: "codex-cli",
+ backend: "hwlab-cloud-api/codex-cli",
+ capabilityLevel: "text-chat-only",
+ sessionMode: "ephemeral-one-shot",
+ runner: {
+ kind: "codex-cli-one-shot-ephemeral"
+ },
+ runnerLimitations: ["one-shot", "not-durable-session"],
+ traceId: "trc_one_shot"
+ }
+ });
+
+ assert.equal(summary.kind, "stateless-one-shot");
+ assert.equal(summary.label, "一次性执行");
+ assert.equal(summary.tone, "warn");
+ assert.equal(summary.sessionMode, "ephemeral-one-shot");
+ assert.equal(summary.lastTraceId, "trc_one_shot");
+});
+
+test("maps HWLAB Skill CLI API control to distinct green state when ready", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ latestMessage: {
+ status: "completed",
+ provider: "hwlab-skill-cli",
+ backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
+ capabilityLevel: "hwlab-api-control-ready",
+ sessionMode: "controlled-m3-io-skill-cli",
+ session: {
+ status: "idle",
+ lastTraceId: "trc_skill"
+ },
+ toolCalls: [
+ {
+ type: "skill-cli",
+ route: "/v1/m3/io",
+ status: "completed",
+ controlReady: true
+ }
+ ],
+ providerTrace: {
+ controlReady: true
+ }
+ }
+ });
+
+ assert.equal(summary.kind, "skill-cli-api-control");
+ assert.equal(summary.label, "HWLAB Skill CLI 控制");
+ assert.equal(summary.tone, "ok");
+ assert.equal(summary.provider, "hwlab-skill-cli");
+ assert.equal(summary.capabilityLevel, "hwlab-api-control-ready");
+});
+
+test("maps blockers to blocked tone and redacts secret-like fields", () => {
+ const summary = classifyCodeAgentStatusSummary({
+ availability: {
+ status: "blocked",
+ ready: false,
+ provider: "openai-responses",
+ backend: "Secret hwlab-code-agent-provider should not show",
+ capabilityLevel: "blocked",
+ reason: "OPENAI_API_KEY_missing",
+ error: {
+ code: "provider_unavailable",
+ providerStatus: 503
+ },
+ missingEnv: ["OPENAI_API_KEY"],
+ traceId: "trc_blocked"
+ }
+ });
+
+ assert.equal(summary.kind, "blocked");
+ assert.equal(summary.label, "服务受阻");
+ assert.equal(summary.tone, "blocked");
+ assert.equal(summary.backend, "已隐藏");
+ assert.equal(summary.readinessBlockers.includes("provider_config_blocked"), true);
+ assert.equal(summary.readinessBlockers.includes("provider_unavailable"), true);
+ assert.equal(summary.readinessBlockers.includes("provider_http_503"), true);
+ assert.equal(summary.lastTraceId, "trc_blocked");
+});
diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html
index 7d43e129..883f88da 100644
--- a/web/hwlab-cloud-web/index.html
+++ b/web/hwlab-cloud-web/index.html
@@ -83,6 +83,15 @@
等待输入
+
+
+ 探测中
+ 未观测
+ trace 未观测
+
+
+