feat: add code agent status summary
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 : [];
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
@@ -83,6 +83,15 @@
|
||||
</div>
|
||||
<span class="state-tag tone-source" id="agent-chat-status">等待输入</span>
|
||||
</div>
|
||||
<details class="code-agent-summary tone-border-pending" id="code-agent-summary">
|
||||
<summary>
|
||||
<span class="code-agent-summary-icon tone-pending" id="code-agent-summary-icon" aria-hidden="true">○</span>
|
||||
<strong class="code-agent-summary-label tone-pending" id="code-agent-summary-label">探测中</strong>
|
||||
<span class="code-agent-summary-capability" id="code-agent-summary-capability">未观测</span>
|
||||
<span class="code-agent-summary-trace" id="code-agent-summary-trace">trace 未观测</span>
|
||||
</summary>
|
||||
<div class="code-agent-summary-detail" id="code-agent-summary-detail"></div>
|
||||
</details>
|
||||
<div class="conversation-list" id="conversation-list" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user