diff --git a/package.json b/package.json index 41670e6a..50941235 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/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/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/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", "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/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 2276ab6f..4c380b3a 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -1,4 +1,11 @@ import { gateSummary } from "./gate-summary.mjs"; +import { + codeAgentFactsFromMessage, + compactHwlabApiFact, + runnerTraceSummary as compactRunnerTraceSummary, + skillsSummary as compactSkillsSummary, + toolCallsSummary as compactToolCallsSummary +} from "./code-agent-facts.mjs"; import { classifyWorkbenchLiveStatus } from "./live-status.mjs"; import { marked } from "./third_party/marked/marked.esm.js"; import { wiringPresentationFromSummary } from "./wiring-status.mjs"; @@ -2397,11 +2404,55 @@ function messageCard(message) { article.append(textSpan(roleLabel(message.role), "message-role")); article.append(textSpan(message.title, "message-title")); article.append(textSpan(message.text, "message-copy")); + const capabilityFacts = codeAgentCapabilityFactsPanel(message); + if (capabilityFacts) article.append(capabilityFacts); const messageEvidence = messageEvidencePanel(message); if (messageEvidence) article.append(messageEvidence); return article; } +function codeAgentCapabilityFactsPanel(message) { + const facts = codeAgentFactsFromMessage(message); + if (!facts) return null; + const section = document.createElement("section"); + section.className = `code-agent-facts tone-border-${toneClass(facts.tone)}`; + section.dataset.codeAgentFactKind = facts.kind; + section.append(codeAgentFactsHeader(facts)); + + const grid = document.createElement("dl"); + grid.className = "code-agent-fact-grid"; + for (const row of facts.rows) { + const item = document.createElement("div"); + item.className = "code-agent-fact-row"; + item.append(textSpan(row.label, "code-agent-fact-key"), textSpan(row.value ?? "none", "code-agent-fact-value mono")); + grid.append(item); + } + section.append(grid); + + if (facts.hwlabApiFacts.length > 0) { + const apiList = document.createElement("div"); + apiList.className = "hwlab-api-facts"; + apiList.append(textSpan("HWLAB API", "hwlab-api-facts-title")); + for (const fact of facts.hwlabApiFacts) { + const code = document.createElement("code"); + code.className = "hwlab-api-fact-code"; + code.textContent = compactHwlabApiFact(fact); + apiList.append(code); + } + section.append(apiList); + } + + return section; +} + +function codeAgentFactsHeader(facts) { + const header = document.createElement("div"); + header.className = "code-agent-facts-head"; + header.append(badge(facts.statusLabel, facts.tone)); + header.append(textSpan(facts.summary, "code-agent-facts-summary")); + return header; +} + function messageEvidencePanel(message) { const fields = messageEvidenceFields(message).map(boundedEvidenceField); if (fields.length === 0) return null; @@ -2464,27 +2515,18 @@ function providerTraceSummary(providerTrace) { } function toolCallsSummary(toolCalls) { - if (!Array.isArray(toolCalls) || toolCalls.length === 0) return null; - const completed = toolCalls.filter((tool) => tool?.status === "completed").length; - const names = toolCalls.map((tool) => tool?.name).filter(Boolean).slice(0, 3).join(","); - return `${completed}/${toolCalls.length}${names ? `:${names}` : ""}`; + const summary = compactToolCallsSummary(toolCalls); + return summary === "none" ? null : summary; } function skillsSummary(skills) { - if (!skills || typeof skills !== "object") return null; - if (skills.status === "not_requested") return "not_requested"; - const count = skills.totalCount ?? skills.count ?? (Array.isArray(skills.items) ? skills.items.length : 0); - return `${skills.status ?? "unknown"}:${count}`; + const summary = compactSkillsSummary(skills); + return summary === "none" ? null : summary; } function runnerTraceSummary(runnerTrace) { - if (!runnerTrace || typeof runnerTrace !== "object") return null; - const parts = [ - runnerTrace.runnerKind ?? "present", - runnerTrace.sessionMode, - typeof runnerTrace.turn === "number" ? `turn${runnerTrace.turn}` : null - ].filter(Boolean); - return parts.join(":"); + const summary = compactRunnerTraceSummary(runnerTrace); + return summary === "none" ? null : summary; } function sessionReuseSummary(sessionReuse) { diff --git a/web/hwlab-cloud-web/code-agent-facts.mjs b/web/hwlab-cloud-web/code-agent-facts.mjs new file mode 100644 index 00000000..a0e30dc6 --- /dev/null +++ b/web/hwlab-cloud-web/code-agent-facts.mjs @@ -0,0 +1,307 @@ +const M3_IO_ROUTE = "/v1/m3/io"; +const TEXT_FALLBACK_RUNNER = "openai-responses-fallback"; +const CODEX_ONE_SHOT_RUNNER = "codex-cli-one-shot-ephemeral"; +const READONLY_SESSION_MODE = "controlled-readonly-session-registry"; +const CODEX_STDIO_SESSION_MODE = "codex-mcp-stdio-long-lived"; +const HWLAB_SKILL_RUNNER = "hwlab-m3-io-skill-cli"; + +export function codeAgentFactsFromMessage(message) { + if (!hasCodeAgentFields(message)) return null; + const classification = classifyCodeAgentFacts(message); + const rows = [ + { + label: "runner/provider/backend", + value: compactFields([ + field("runner", message.runner?.kind ?? runnerKindFromMessage(message)), + field("provider", message.provider), + field("backend", message.backend) + ]) + }, + { + label: "session", + value: compactFields([ + field("id", message.session?.sessionId ?? message.sessionId ?? message.runner?.sessionId), + field("status", message.session?.status), + field("sessionMode", message.sessionMode ?? message.runner?.sessionMode ?? message.runner?.session) + ]) + }, + { + label: "workspace", + value: message.workspace ?? message.runner?.workspace ?? "none" + }, + { + label: "sandbox", + value: message.sandbox ?? message.runner?.sandbox ?? "none" + }, + { + label: "capabilityLevel", + value: message.capabilityLevel ?? message.runner?.capabilityLevel ?? "unknown" + }, + { + label: "toolCalls", + value: toolCallsSummary(message.toolCalls) + }, + { + label: "skills", + value: skillsSummary(message.skills) + }, + { + label: "runnerTrace/traceId", + value: compactFields([ + field("traceId", message.traceId), + field("runnerTrace", runnerTraceSummary(message.runnerTrace)) + ]) + } + ]; + + return { + ...classification, + rows, + hwlabApiFacts: hwlabApiFactsFromMessage(message) + }; +} + +export function classifyCodeAgentFacts(message) { + const runnerKind = runnerKindFromMessage(message); + const provider = String(message?.provider ?? "").trim(); + const sessionMode = String(message?.sessionMode ?? message?.runner?.sessionMode ?? message?.runner?.session ?? "").trim(); + const implementationType = String(message?.implementationType ?? message?.runner?.implementationType ?? "").trim(); + const capabilityLevel = String(message?.capabilityLevel ?? message?.runner?.capabilityLevel ?? "").trim(); + const longLivedGate = message?.longLivedSessionGate; + + if (provider === "codex-stdio" && longLivedGate?.status === "pass" && longLivedGate?.pass === true) { + return { + kind: "codex-stdio-long-lived", + statusLabel: "完整 Code Agent session", + tone: "dev-live", + summary: "Codex stdio long-lived session gate passed; session lifecycle and trace capture are available.", + fullCodeAgent: true, + codexSessionGatePass: true + }; + } + + if (provider === "hwlab-skill-cli" || runnerKind === HWLAB_SKILL_RUNNER || hasM3IoToolCall(message?.toolCalls)) { + return { + kind: "hwlab-skill-cli", + statusLabel: "HWLAB Skill CLI", + tone: "source", + summary: "Controlled Skill CLI -> HWLAB API path only; this is not OpenAI fallback and not a generic Codex session.", + fullCodeAgent: false, + codexSessionGatePass: false + }; + } + + if (runnerKind === CODEX_ONE_SHOT_RUNNER || sessionMode === "ephemeral-one-shot" || implementationType === "codex-cli-one-shot-ephemeral") { + return { + kind: "stateless-one-shot", + statusLabel: "stateless-one-shot", + tone: "source", + summary: "Ephemeral one-shot runner result; do not treat it as a reusable or long-lived Code Agent session.", + fullCodeAgent: false, + codexSessionGatePass: false + }; + } + + if (runnerKind === TEXT_FALLBACK_RUNNER || provider === "openai-responses" || capabilityLevel === "text-chat-only") { + return { + kind: "text-chat-only", + statusLabel: "openai-responses-fallback / text-chat-only", + tone: "source", + summary: "Text chat fallback can answer ordinary messages, but it has no workspace, tools, skills, or Codex session gate.", + fullCodeAgent: false, + codexSessionGatePass: false + }; + } + + if (provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || sessionMode === READONLY_SESSION_MODE) { + return { + kind: READONLY_SESSION_MODE, + statusLabel: "只读 session registry", + tone: "source", + summary: "Bounded read-only tools and session mapping are available, but Codex stdio, write capability, and durable session remain blocked.", + fullCodeAgent: false, + codexSessionGatePass: false + }; + } + + if (provider === "codex-stdio" || sessionMode === CODEX_STDIO_SESSION_MODE) { + return { + kind: "codex-stdio-blocked", + statusLabel: "Codex stdio blocked", + tone: "blocked", + summary: "Codex stdio fields are present, but the long-lived session gate has not passed.", + fullCodeAgent: false, + codexSessionGatePass: false + }; + } + + return { + kind: "unknown-code-agent-path", + statusLabel: "Code Agent facts", + tone: message?.status === "failed" ? "blocked" : "source", + summary: "Provider response is shown with its raw runner/session facts; completion is not promoted without a passing gate.", + fullCodeAgent: false, + codexSessionGatePass: false + }; +} + +export function hwlabApiFactsFromMessage(message) { + const facts = []; + for (const toolCall of Array.isArray(message?.toolCalls) ? message.toolCalls : []) { + const parsed = parseToolStdout(toolCall.stdout); + const route = firstText(toolCall.route, toolCall.hwlabApi?.route, parsed?.route); + if (route !== M3_IO_ROUTE) continue; + const blocker = firstText( + toolCall.blocker?.code, + toolCall.capabilityBlocker?.code, + toolCall.trustBlocker?.code, + firstBlockerCode(toolCall.blockers), + parsed?.blocker?.code, + parsed?.blocker, + parsed?.durable?.blocker + ); + facts.push({ + tool: firstText(toolCall.name, "skill-cli"), + route, + operationId: nullableText(toolCall.operationId ?? parsed?.operationId), + traceId: nullableText(toolCall.traceId ?? parsed?.traceId ?? message?.traceId), + auditId: nullableText(toolCall.audit?.auditId ?? parsed?.audit?.auditId ?? parsed?.auditId), + evidenceId: nullableText(toolCall.evidence?.evidenceId ?? parsed?.evidence?.evidenceId ?? parsed?.evidenceId), + blocker: nullableText(blocker), + status: firstText(toolCall.status, parsed?.status, "unknown") + }); + } + return facts; +} + +export function compactHwlabApiFact(fact) { + if (!fact) return ""; + return compactFields([ + field("route", fact.route), + field("operationId", fact.operationId), + field("traceId", fact.traceId), + field("auditId", fact.auditId), + field("evidenceId", fact.evidenceId), + field("blocker", fact.blocker) + ]); +} + +export function toolCallsSummary(toolCalls) { + if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none"; + const completed = toolCalls.filter((tool) => tool?.status === "completed").length; + const names = toolCalls + .map((tool) => compactFields([ + firstText(tool?.name, "tool"), + tool?.status ? `(${tool.status})` : null, + tool?.route ? `@${tool.route}` : null + ], "")) + .filter(Boolean) + .slice(0, 4) + .join(", "); + return `${completed}/${toolCalls.length}${names ? ` ${names}` : ""}`; +} + +export function skillsSummary(skills) { + if (!skills || typeof skills !== "object") return "none"; + const count = skills.totalCount ?? skills.count ?? (Array.isArray(skills.items) ? skills.items.length : 0); + const names = Array.isArray(skills.items) + ? skills.items.map((item) => item?.name).filter(Boolean).slice(0, 3).join(",") + : ""; + const blockers = Array.isArray(skills.blockers) + ? skills.blockers.map((blocker) => blocker?.code).filter(Boolean).slice(0, 2).join(",") + : ""; + return compactFields([ + skills.status ?? "unknown", + `count=${count}`, + names ? `items=${names}` : null, + blockers ? `blockers=${blockers}` : null + ], ":"); +} + +export function runnerTraceSummary(runnerTrace) { + if (!runnerTrace || typeof runnerTrace !== "object") return "none"; + return compactFields([ + field("traceId", runnerTrace.traceId), + field("runnerKind", runnerTrace.runnerKind), + field("sessionMode", runnerTrace.sessionMode), + typeof runnerTrace.turn === "number" ? field("turn", runnerTrace.turn) : null, + field("route", runnerTrace.route), + field("status", runnerTrace.status) + ]); +} + +function hasCodeAgentFields(message) { + return Boolean( + message && + typeof message === "object" && + ( + message.provider || + message.backend || + message.runner || + message.session || + message.sessionMode || + message.capabilityLevel || + Array.isArray(message.toolCalls) || + message.runnerTrace + ) + ); +} + +function hasM3IoToolCall(toolCalls) { + return Array.isArray(toolCalls) && toolCalls.some((toolCall) => + toolCall?.route === M3_IO_ROUTE || + toolCall?.hwlabApi?.route === M3_IO_ROUTE || + parseToolStdout(toolCall?.stdout)?.route === M3_IO_ROUTE + ); +} + +function runnerKindFromMessage(message) { + return firstText( + message?.runner?.kind, + message?.runnerTrace?.runnerKind, + message?.provider === "openai-responses" ? TEXT_FALLBACK_RUNNER : null + ); +} + +function parseToolStdout(stdout) { + if (typeof stdout !== "string" || !stdout.trim()) return null; + const text = stdout.trim(); + const direct = parseJson(text); + if (direct) return direct; + const start = text.indexOf("{"); + const end = text.lastIndexOf("}"); + if (start === -1 || end <= start) return null; + return parseJson(text.slice(start, end + 1)); +} + +function parseJson(text) { + try { + const parsed = JSON.parse(text); + return parsed && typeof parsed === "object" ? parsed : null; + } catch { + return null; + } +} + +function firstBlockerCode(blockers) { + if (!Array.isArray(blockers)) return null; + return blockers.map((blocker) => blocker?.code).find(Boolean) ?? null; +} + +function compactFields(fields, separator = " / ") { + return fields.filter((item) => item !== null && item !== undefined && String(item).trim()).map((item) => String(item).trim()).join(separator); +} + +function field(label, value) { + const normalized = nullableText(value); + return normalized ? `${label}=${normalized}` : null; +} + +function firstText(...values) { + return values.map(nullableText).find(Boolean) ?? null; +} + +function nullableText(value) { + if (value === undefined || value === null || value === "") return null; + return String(value).replace(/\s+/gu, " ").trim(); +} diff --git a/web/hwlab-cloud-web/code-agent-facts.test.mjs b/web/hwlab-cloud-web/code-agent-facts.test.mjs new file mode 100644 index 00000000..2068a832 --- /dev/null +++ b/web/hwlab-cloud-web/code-agent-facts.test.mjs @@ -0,0 +1,256 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + codeAgentFactsFromMessage, + compactHwlabApiFact +} from "./code-agent-facts.mjs"; + +test("codex-readonly-runner is shown as partial read-only session registry", () => { + const facts = codeAgentFactsFromMessage({ + status: "source", + provider: "codex-readonly-runner", + model: "read-only-tools", + backend: "hwlab-cloud-api/codex-readonly-runner", + workspace: "/workspace/hwlab", + sandbox: "read-only", + capabilityLevel: "read-only-session-tools", + sessionMode: "controlled-readonly-session-registry", + implementationType: "controlled-readonly-session-registry", + session: { + sessionId: "ses_readonly", + status: "idle", + turn: 1, + lastTraceId: "trc_readonly" + }, + sessionReuse: { + reused: false, + turn: 1, + status: "idle" + }, + runner: { + kind: "hwlab-readonly-runner", + codexStdio: false, + writeCapable: false, + durableSession: false + }, + longLivedSessionGate: { + status: "blocked", + pass: false, + blockers: [{ code: "controlled_readonly_not_long_lived_stdio" }] + }, + toolCalls: [{ name: "pwd", status: "completed" }], + skills: { status: "not_requested", count: 0 }, + runnerTrace: { + traceId: "trc_readonly", + runnerKind: "hwlab-readonly-runner", + sessionMode: "controlled-readonly-session-registry", + turn: 1 + }, + traceId: "trc_readonly" + }); + + assert.equal(facts.kind, "controlled-readonly-session-registry"); + assert.equal(facts.fullCodeAgent, false); + assert.equal(facts.codexSessionGatePass, false); + assert.match(facts.summary, /Codex stdio/u); + assert.match(facts.rows.find((row) => row.label === "runner/provider/backend").value, /runner=hwlab-readonly-runner/u); + assert.match(facts.rows.find((row) => row.label === "toolCalls").value, /1\/1 pwd\(completed\)/u); +}); + +test("long-lived Codex stdio session fields are shown as full Code Agent session", () => { + const facts = codeAgentFactsFromMessage({ + status: "completed", + provider: "codex-stdio", + model: "gpt-5.5", + backend: "hwlab-cloud-api/codex-mcp-stdio", + workspace: "/workspace/hwlab", + sandbox: "workspace-write", + capabilityLevel: "long-lived-codex-stdio-session", + sessionMode: "codex-mcp-stdio-long-lived", + implementationType: "repo-owned-codex-mcp-stdio-session", + session: { + sessionId: "ses_stdio", + status: "idle", + turn: 3, + idleTimeoutMs: 1800000, + lastTraceId: "trc_stdio", + longLivedSession: true, + codexStdio: true + }, + runner: { + kind: "codex-mcp-stdio-runner", + codexStdio: true, + writeCapable: true, + durableSession: true + }, + longLivedSessionGate: { + status: "pass", + pass: true + }, + toolCalls: [{ name: "codex-reply", status: "completed" }], + skills: { status: "not_requested", count: 0 }, + runnerTrace: { + traceId: "trc_stdio", + runnerKind: "codex-mcp-stdio-runner", + sessionMode: "codex-mcp-stdio-long-lived", + turn: 3 + }, + traceId: "trc_stdio" + }); + + assert.equal(facts.kind, "codex-stdio-long-lived"); + assert.equal(facts.statusLabel, "完整 Code Agent session"); + assert.equal(facts.fullCodeAgent, true); + assert.equal(facts.codexSessionGatePass, true); + assert.match(facts.rows.find((row) => row.label === "sandbox").value, /workspace-write/u); +}); + +test("openai-responses fallback is explicitly text-chat-only", () => { + const facts = codeAgentFactsFromMessage({ + status: "source", + provider: "openai-responses", + model: "gpt-5.5", + backend: "hwlab-cloud-api/openai-responses", + capabilityLevel: "text-chat-only", + sessionMode: "provider-text-request", + runner: { + kind: "openai-responses-fallback", + codexStdio: false, + writeCapable: false, + durableSession: false + }, + longLivedSessionGate: { + status: "blocked", + pass: false, + blockers: [{ code: "openai_responses_fallback_not_session" }] + }, + toolCalls: [], + skills: { status: "not_requested", count: 0 }, + runnerTrace: { + traceId: "trc_fallback", + runnerKind: "openai-responses-fallback", + events: ["fallback:text-chat-only"] + }, + traceId: "trc_fallback" + }); + + assert.equal(facts.kind, "text-chat-only"); + assert.equal(facts.statusLabel, "openai-responses-fallback / text-chat-only"); + assert.equal(facts.fullCodeAgent, false); + assert.equal(facts.codexSessionGatePass, false); + assert.match(facts.summary, /no workspace, tools, skills, or Codex session gate/u); +}); + +test("stateless one-shot is not wrapped as a long-lived session", () => { + const facts = codeAgentFactsFromMessage({ + status: "source", + provider: "codex-cli", + model: "gpt-5.5", + backend: "hwlab-cloud-api/codex-cli", + workspace: "/workspace/hwlab", + sandbox: "read-only", + capabilityLevel: "text-chat-only", + sessionMode: "ephemeral-one-shot", + implementationType: "codex-cli-one-shot-ephemeral", + session: { + sessionId: "cnv_one_shot", + status: "expired", + idleTimeoutMs: 0, + lastTraceId: "trc_one_shot" + }, + runner: { + kind: "codex-cli-one-shot-ephemeral", + codexStdio: false, + writeCapable: false, + durableSession: false + }, + longLivedSessionGate: { + status: "blocked", + pass: false, + blockers: [{ code: "one_shot_runner_not_long_lived" }] + }, + traceId: "trc_one_shot" + }); + + assert.equal(facts.kind, "stateless-one-shot"); + assert.equal(facts.fullCodeAgent, false); + assert.equal(facts.codexSessionGatePass, false); + assert.match(facts.summary, /Ephemeral one-shot runner/u); +}); + +test("HWLAB Skill CLI toolCall shows compact copyable /v1/m3/io facts", () => { + const facts = codeAgentFactsFromMessage({ + status: "source", + provider: "hwlab-skill-cli", + model: "controlled-m3-io", + backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli", + workspace: "/workspace/hwlab", + sandbox: "hwlab-api-route-only", + capabilityLevel: "hwlab-api-control-ready", + sessionMode: "controlled-m3-io-skill-cli", + runner: { + kind: "hwlab-m3-io-skill-cli", + codexStdio: false, + writeCapable: true, + durableSession: false + }, + longLivedSessionGate: { + status: "blocked", + pass: false + }, + toolCalls: [ + { + name: "hwlab-agent-runtime.m3-io", + type: "skill-cli", + status: "completed", + route: "/v1/m3/io", + operationId: "op_m3_do_write_cli", + traceId: "trc_m3_skill_cli", + audit: { auditId: "aud_m3_do_write_cli_succeeded" }, + evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" }, + blocker: { code: "runtime_durable_not_green" }, + stdout: JSON.stringify({ + route: "/v1/m3/io", + status: "succeeded", + traceId: "trc_m3_skill_cli", + operationId: "op_m3_do_write_cli", + audit: { auditId: "aud_m3_do_write_cli_succeeded" }, + evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" }, + durable: { blocker: "runtime_durable_not_green" } + }) + } + ], + skills: { + status: "used", + count: 1, + items: [{ name: "hwlab-agent-runtime.m3-io" }] + }, + runnerTrace: { + traceId: "trc_m3_skill_cli", + runnerKind: "hwlab-m3-io-skill-cli", + sessionMode: "controlled-m3-io-skill-cli", + route: "/v1/m3/io", + status: "succeeded" + }, + traceId: "trc_m3_skill_cli" + }); + + assert.equal(facts.kind, "hwlab-skill-cli"); + assert.equal(facts.fullCodeAgent, false); + assert.equal(facts.hwlabApiFacts.length, 1); + assert.deepEqual(facts.hwlabApiFacts[0], { + tool: "hwlab-agent-runtime.m3-io", + route: "/v1/m3/io", + operationId: "op_m3_do_write_cli", + traceId: "trc_m3_skill_cli", + auditId: "aud_m3_do_write_cli_succeeded", + evidenceId: "evd_m3_do_write_cli_succeeded", + blocker: "runtime_durable_not_green", + status: "completed" + }); + assert.equal( + compactHwlabApiFact(facts.hwlabApiFacts[0]), + "route=/v1/m3/io / operationId=op_m3_do_write_cli / traceId=trc_m3_skill_cli / auditId=aud_m3_do_write_cli_succeeded / evidenceId=evd_m3_do_write_cli_succeeded / blocker=runtime_durable_not_green" + ); +}); diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index 4a8bb1a5..95a9654b 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 wiring-status.test.mjs && node scripts/check.mjs", + "check": "node --test code-agent-facts.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 16ec807f..e3434374 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -22,6 +22,7 @@ const requiredFiles = [ "index.html", "styles.css", "app.mjs", + "code-agent-facts.mjs", "live-status.mjs", "gate-summary.mjs", "runtime.mjs", @@ -42,6 +43,7 @@ for (const file of requiredFiles) { 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 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"); @@ -640,6 +642,19 @@ assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 D assert.match(app, /function messageEvidencePanel/); assert.match(app, /function messageEvidenceSummary/); assert.match(app, /function boundedEvidenceField/); +assert.match(app, /codeAgentFactsFromMessage/); +assert.match(app, /function codeAgentCapabilityFactsPanel/); +assert.match(app, /compactHwlabApiFact/); +assert.match(codeAgentFacts, /openai-responses-fallback/); +assert.match(codeAgentFacts, /text-chat-only/); +assert.match(codeAgentFacts, /stateless-one-shot/); +assert.match(codeAgentFacts, /controlled-readonly-session-registry/); +assert.match(codeAgentFacts, /hwlab-m3-io-skill-cli/); +assert.match(codeAgentFacts, /route", fact\.route/); +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(app, /sourceKind:\s*completion\.sourceKind/); assert.match(app, /status === "completed" \? "dev-live"/); assert.doesNotMatch(app, /status === "failed" \? "dev-live"/); @@ -652,12 +667,17 @@ assert.match(styles, /\.message-user\s*{/); assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/); assert.match(styles, /\.message-evidence\s*{/); assert.match(styles, /\.message-evidence-chip\s*{/); +assert.match(styles, /\.code-agent-facts\s*{/); +assert.match(styles, /\.code-agent-fact-grid\s*{/); +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); assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/s); assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s); assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s); assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-evidence\s*{[\s\S]*?grid-column:\s*1;/); +assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.code-agent-fact-grid\s*{[\s\S]*?grid-template-columns:\s*1fr;/); for (const userFacingFunctionName of [ "codeAgentStatusMessage", "codeAgentPromptText", diff --git a/web/hwlab-cloud-web/scripts/dist-contract.mjs b/web/hwlab-cloud-web/scripts/dist-contract.mjs index 65e453e5..04ffd7db 100644 --- a/web/hwlab-cloud-web/scripts/dist-contract.mjs +++ b/web/hwlab-cloud-web/scripts/dist-contract.mjs @@ -9,6 +9,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([ "index.html", "styles.css", "app.mjs", + "code-agent-facts.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 d1a7c7e9..5b5993c5 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -663,6 +663,99 @@ h3 { min-width: 0; } +.code-agent-facts { + grid-column: 2; + min-width: 0; + display: grid; + gap: 7px; + padding: 8px; + background: rgba(15, 17, 16, 0.72); + border: 1px solid var(--line); + border-left-width: 3px; +} + +.code-agent-facts-head { + min-width: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 7px; + align-items: start; +} + +.code-agent-facts-summary { + min-width: 0; + color: var(--muted); + font-size: 11px; + overflow-wrap: anywhere; +} + +.code-agent-fact-grid { + min-width: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px; + margin: 0; +} + +.code-agent-fact-row { + min-width: 0; + display: grid; + gap: 2px; + padding: 6px; + background: var(--surface); + border: 1px solid var(--line); +} + +.code-agent-fact-key, +.code-agent-fact-value { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.code-agent-fact-key { + color: var(--dim); + font-family: var(--mono); + font-size: 9px; + font-weight: 760; + text-transform: uppercase; +} + +.code-agent-fact-value { + color: var(--text); + font-size: 10px; +} + +.hwlab-api-facts { + min-width: 0; + display: grid; + gap: 5px; +} + +.hwlab-api-facts-title { + color: var(--accent-2); + font-family: var(--mono); + font-size: 9px; + font-weight: 760; + text-transform: uppercase; +} + +.hwlab-api-fact-code { + min-width: 0; + display: block; + padding: 6px; + background: var(--surface); + border: 1px solid var(--line); + color: var(--text); + font-family: var(--mono); + font-size: 10px; + line-height: 1.35; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + user-select: all; +} + .message-evidence summary { cursor: pointer; list-style-position: inside; @@ -1628,9 +1721,15 @@ tbody tr:last-child td { grid-template-columns: 1fr; } + .code-agent-facts, .message-evidence { grid-column: 1; } + + .code-agent-facts-head, + .code-agent-fact-grid { + grid-template-columns: 1fr; + } } @media (max-width: 520px) {