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 未观测 + +
+
diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index 95a9654b..4801b663 100644 --- a/web/hwlab-cloud-web/package.json +++ b/web/hwlab-cloud-web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "m3-readonly": "node scripts/m3-readonly-contract.mjs", - "check": "node --test code-agent-facts.test.mjs wiring-status.test.mjs && node scripts/check.mjs", + "check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs && node scripts/check.mjs", "build": "node scripts/build.mjs", "layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report ../../reports/dev-gate/dev-cloud-workbench-layout.json", "layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report ../../reports/dev-gate/dev-cloud-workbench-layout-build.json", diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index e3434374..99c68730 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -23,6 +23,7 @@ const requiredFiles = [ "styles.css", "app.mjs", "code-agent-facts.mjs", + "code-agent-status.mjs", "live-status.mjs", "gate-summary.mjs", "runtime.mjs", @@ -44,6 +45,7 @@ const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8"); const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8"); const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8"); const codeAgentFacts = fs.readFileSync(path.resolve(rootDir, "code-agent-facts.mjs"), "utf8"); +const codeAgentStatus = fs.readFileSync(path.resolve(rootDir, "code-agent-status.mjs"), "utf8"); const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8"); const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-status.mjs"), "utf8"); const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8"); @@ -325,6 +327,10 @@ for (const workbenchElement of [ "resource-tree", "conversation-list", "agent-chat-status", + "code-agent-summary", + "code-agent-summary-label", + "code-agent-summary-capability", + "code-agent-summary-trace", "task-list", "right-sidebar", "right-sidebar-resize", @@ -645,6 +651,11 @@ assert.match(app, /function boundedEvidenceField/); assert.match(app, /codeAgentFactsFromMessage/); assert.match(app, /function codeAgentCapabilityFactsPanel/); assert.match(app, /compactHwlabApiFact/); +assert.match(app, /function renderCodeAgentSummary/); +assert.match(app, /currentCodeAgentStatusSummary/); +assert.match(app, /classifyCodeAgentStatusSummary/); +assert.match(app, /codeAgent\.status/); +assert.match(app, /当前部署 revision/); assert.match(codeAgentFacts, /openai-responses-fallback/); assert.match(codeAgentFacts, /text-chat-only/); assert.match(codeAgentFacts, /stateless-one-shot/); @@ -655,6 +666,14 @@ assert.match(codeAgentFacts, /operationId", fact\.operationId/); assert.match(codeAgentFacts, /auditId", fact\.auditId/); assert.match(codeAgentFacts, /evidenceId", fact\.evidenceId/); assert.match(codeAgentFacts, /blocker", fact\.blocker/); +assert.match(codeAgentStatus, /fallback-text-chat-only/); +assert.match(codeAgentStatus, /stateless-one-shot/); +assert.match(codeAgentStatus, /read-only-session-tools/); +assert.match(codeAgentStatus, /skill-cli-api-control/); +assert.match(codeAgentStatus, /currentDeploymentRevision/); +assert.match(codeAgentStatus, /unsafeGreenForNonReady/); +assert.match(codeAgentStatus, /provider_config_blocked/); +assert.doesNotMatch(codeAgentStatus, /sourceMain|SOURCE main|待部署 commit|targetCommit/u); assert.match(app, /sourceKind:\s*completion\.sourceKind/); assert.match(app, /status === "completed" \? "dev-live"/); assert.doesNotMatch(app, /status === "failed" \? "dev-live"/); @@ -669,6 +688,10 @@ assert.match(styles, /\.message-evidence\s*{/); assert.match(styles, /\.message-evidence-chip\s*{/); assert.match(styles, /\.code-agent-facts\s*{/); assert.match(styles, /\.code-agent-fact-grid\s*{/); +assert.match(styles, /\.code-agent-summary\s*{/); +assert.match(styles, /\.tone-border-warn/); +assert.match(styles, /\.tone-border-blocked/); +assert.match(styles, /\.tone-border-ok/); assert.match(styles, /\.hwlab-api-fact-code\s*{/); assert.match(styles, /\.hwlab-api-fact-code\s*{[^}]*user-select:\s*all;/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); diff --git a/web/hwlab-cloud-web/scripts/dist-contract.mjs b/web/hwlab-cloud-web/scripts/dist-contract.mjs index 04ffd7db..dcfb43dc 100644 --- a/web/hwlab-cloud-web/scripts/dist-contract.mjs +++ b/web/hwlab-cloud-web/scripts/dist-contract.mjs @@ -10,6 +10,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([ "styles.css", "app.mjs", "code-agent-facts.mjs", + "code-agent-status.mjs", "live-status.mjs", "gate-summary.mjs", "runtime.mjs", diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 5b5993c5..74f76f21 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -527,7 +527,10 @@ h3 { padding: 12px; } -.conversation-panel, +.conversation-panel { + grid-template-rows: auto auto minmax(0, 1fr); +} + .task-panel { grid-template-rows: auto minmax(0, 1fr); } @@ -580,6 +583,99 @@ h3 { gap: 7px; } +.code-agent-summary { + min-width: 0; + display: grid; + gap: 8px; + padding: 8px 10px; + background: rgba(15, 17, 16, 0.72); + border: 1px solid var(--line); + border-left-width: 3px; +} + +.code-agent-summary summary { + min-width: 0; + display: grid; + grid-template-columns: auto minmax(88px, auto) minmax(120px, 1fr) minmax(110px, auto); + gap: 8px; + align-items: center; + cursor: pointer; + list-style: none; +} + +.code-agent-summary summary::-webkit-details-marker { + display: none; +} + +.code-agent-summary summary::after { + content: "展开"; + justify-self: end; + color: var(--dim); + font-size: 10px; +} + +.code-agent-summary[open] summary::after { + content: "收起"; +} + +.code-agent-summary-icon { + font-family: var(--mono); + font-size: 13px; + line-height: 1; +} + +.code-agent-summary-label { + min-width: 0; + font-size: 12px; + overflow-wrap: anywhere; +} + +.code-agent-summary-capability, +.code-agent-summary-trace { + min-width: 0; + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + +.code-agent-summary-trace { + justify-self: end; + text-align: right; +} + +.code-agent-summary-detail { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px 8px; +} + +.code-agent-summary-row { + min-width: 0; + display: grid; + grid-template-columns: minmax(92px, 0.8fr) minmax(0, 1.2fr); + gap: 6px; + align-items: start; + padding: 5px 6px; + background: var(--surface); + border: 1px solid var(--line-soft); +} + +.code-agent-summary-key { + min-width: 0; + color: var(--dim); + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + +.code-agent-summary-value { + min-width: 0; + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + .message-card, .info-card, .metric { @@ -1526,10 +1622,16 @@ tbody tr:last-child td { } .tone-border-source, +.tone-border-pending, .tone-border-disabled { border-left-color: var(--dim); } +.tone-border-warn, +.tone-border-warning { + border-left-color: var(--warn); +} + .tone-border-degraded, .tone-border-blocked, .tone-border-failed { @@ -1727,9 +1829,21 @@ tbody tr:last-child td { } .code-agent-facts-head, + .code-agent-fact-grid, + .code-agent-summary summary, + .code-agent-summary-detail, + .code-agent-summary-row { + grid-template-columns: 1fr; + } + .code-agent-fact-grid { grid-template-columns: 1fr; } + + .code-agent-summary-trace { + justify-self: start; + text-align: left; + } } @media (max-width: 520px) {