diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts index b42ed397..01d79650 100644 --- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts +++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts @@ -5,6 +5,16 @@ import { describe, expect, it } from "vitest"; import WorkbenchMessageCard from "./WorkbenchMessageCard.vue"; +function componentStubs() { + return { + TraceTimeline: { template: "
" }, + LoadingState: { template: "
" }, + MessageMarkdown: { template: "
" }, + ApiErrorDiagnostic: { template: "
" }, + StatusBadge: { template: "
" } + }; +} + describe("WorkbenchMessageCard semantic retry visibility", () => { it("shows Git mirror clone progress as runtime preparation", () => { const wrapper = mount(WorkbenchMessageCard, { @@ -41,11 +51,13 @@ describe("WorkbenchMessageCard semantic retry visibility", () => { } } }, - global: { stubs: { TraceTimeline: true, LoadingState: true, MessageMarkdown: true, ApiErrorDiagnostic: true, StatusBadge: true } } + global: { stubs: componentStubs() } }); expect(wrapper.text()).toContain("运行环境准备:正在从 Git mirror 获取 Runner 源码"); expect(wrapper.text()).toContain("第 1/3 次源码获取进行中"); expect(wrapper.text()).not.toContain("基础设施故障"); + expect(wrapper.get(".semantic-failure-banner").attributes("data-emphasis")).toBe("quiet"); + expect(wrapper.find(".semantic-failure-banner code").exists()).toBe(false); wrapper.unmount(); }); @@ -86,17 +98,12 @@ describe("WorkbenchMessageCard semantic retry visibility", () => { } }, global: { - stubs: { - TraceTimeline: true, - LoadingState: true, - MessageMarkdown: true, - ApiErrorDiagnostic: true, - StatusBadge: true - } + stubs: componentStubs() } }); const banner = wrapper.get(".semantic-failure-banner"); + expect(banner.attributes("data-emphasis")).toBe("alert"); expect(banner.text()).toContain("基础设施故障:Runner 镜像仓库暂时不可达"); expect(banner.text()).toContain("第 2/5 次重试已安排"); expect(banner.text()).toMatch(/预计 \d+ 秒后开始/u); diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue index 1bb82dea..da1254c2 100644 --- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue +++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue @@ -123,7 +123,7 @@ function traceStorageKey(message: ChatMessage): string { return `${props.storageKeyPrefix}.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`; } -function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null { +function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; emphasis: "quiet" | "alert"; code: string | null } | null { const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : []; const event = [...events].reverse().find((item) => typeof item.failureDomain === "string" && typeof item.retryPhase === "string"); if (!event) return null; @@ -135,10 +135,10 @@ function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): const progress = attempt !== null && maxAttempts !== null ? `第 ${attempt}/${maxAttempts} 次` : "本次"; const code = firstText(event.failureCode, event.code); if (code === "git-mirror-fetch-in-progress") { - return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", code }; + return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", emphasis: "quiet", code }; } if (code === "git-mirror-fetch-completed") { - return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", code }; + return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", emphasis: "quiet", code }; } let detail = "正在判定有限重试"; let tone = "warning"; @@ -159,6 +159,7 @@ function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): title: `${domain}:${summary}`, detail, tone, + emphasis: "alert", code }; } @@ -198,10 +199,11 @@ function finiteInteger(value: unknown): number | null {
-
+
+
@@ -222,15 +224,53 @@ function finiteInteger(value: unknown): number | null { padding: 0.55rem 0.7rem; } +.semantic-status-marker { + display: none; +} + +.semantic-failure-banner[data-emphasis="quiet"] { + display: flex; + align-items: center; + gap: 0.45rem; + min-width: 0; + margin: 0.25rem 0; + border-left-width: 2px; + border-radius: 0.2rem; + background: transparent; + padding: 0.2rem 0.45rem; + color: #475569; + font-size: 0.82rem; + line-height: 1.35; +} + +.semantic-failure-banner[data-emphasis="quiet"] .semantic-status-marker { + display: block; + flex: 0 0 0.42rem; + width: 0.42rem; + height: 0.42rem; + border-radius: 50%; + background: currentColor; +} + +.semantic-failure-banner[data-emphasis="quiet"] strong { + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.semantic-failure-banner[data-emphasis="quiet"] span:not(.semantic-status-marker) { + flex: 0 0 auto; + color: #64748b; +} + .semantic-failure-banner[data-tone="recovered"] { border-left-color: #15803d; - background: #f0fdf4; color: #166534; } .semantic-failure-banner[data-tone="info"] { border-left-color: #2563eb; - background: #eff6ff; color: #1e40af; }