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 0dcbe9c7..4762e53f 100644
--- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts
+++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts
@@ -46,6 +46,41 @@ describe("WorkbenchMessageCard startup visibility", () => {
});
});
+describe("WorkbenchMessageCard metadata layout", () => {
+ it("renders recent activity at the footer left and elapsed time at the footer right", () => {
+ const now = Date.now();
+ const wrapper = mount(WorkbenchMessageCard, {
+ props: {
+ detailEnabled: false,
+ message: {
+ id: "msg_timing_layout",
+ messageId: "msg_timing_layout",
+ role: "agent",
+ title: "Code Agent",
+ text: "",
+ status: "running",
+ traceId: "trc_timing_layout",
+ sessionId: "ses_timing_layout",
+ createdAt: new Date(now - 10_000).toISOString(),
+ timing: {
+ startedAt: new Date(now - 10_000).toISOString(),
+ lastEventAt: new Date(now - 4_000).toISOString()
+ }
+ }
+ },
+ global: { stubs: componentStubs() }
+ });
+
+ const footer = wrapper.get(".message-meta-footer");
+ expect(wrapper.find("article > header").exists()).toBe(false);
+ expect(footer.get(".message-activity-meta").text()).toMatch(/^最近更新:\d+ 秒前$/u);
+ expect(footer.get(".message-duration-meta").text()).toMatch(/^耗时 \d+ 秒$/u);
+ expect(footer.get(".message-footer-timing").element.firstElementChild).toBe(footer.get(".message-activity-meta").element);
+ expect(footer.get(".message-footer-timing").element.lastElementChild).toBe(footer.get(".message-duration-meta").element);
+ wrapper.unmount();
+ });
+});
+
describe("WorkbenchMessageCard semantic retry visibility", () => {
it("shows Git mirror clone progress as runtime preparation", () => {
const wrapper = mount(WorkbenchMessageCard, {
diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
index 8ad56495..e7d78afd 100644
--- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
+++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
@@ -78,7 +78,7 @@ function messageActivityMeta(message: ChatMessage): { text: string; label: strin
const ageMs = durationSince(timing?.lastEventAt);
if (ageMs === null) return null;
const value = formatDuration(ageMs) + "前";
- return { text: `最近 ${value}`, label: `最近事件:${value}` };
+ return { text: `最近更新:${value}`, label: `最近更新:${value}` };
}
function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
@@ -136,18 +136,6 @@ function traceStorageKey(message: ChatMessage): string {
:data-trace-id="message.traceId || message.runnerTrace?.traceId || undefined"
:data-turn-id="message.turnId || message.traceId || message.runnerTrace?.traceId || undefined"
>
-
{{ semanticRuntimeStatus.title }}
@@ -158,6 +146,20 @@ function traceStorageKey(message: ChatMessage): string {
+
diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css
index 7ed1be8e..e1b9df6a 100644
--- a/web/hwlab-cloud-web/src/styles/workbench.css
+++ b/web/hwlab-cloud-web/src/styles/workbench.css
@@ -493,7 +493,7 @@
line-height: 1.4;
}
-.message-card header,
+.message-meta-footer,
.trace-timeline header {
display: flex;
min-width: 0;
@@ -502,6 +502,14 @@
gap: 10px;
}
+.message-meta-footer {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ margin-top: 6px;
+ border-top: 1px solid #e2e8f0;
+ padding-top: 6px;
+}
+
.message-header-main {
display: flex;
min-width: 0;
@@ -554,6 +562,24 @@
gap: 8px;
}
+.message-footer-timing {
+ display: grid;
+ grid-column: 1 / -1;
+ grid-template-columns: minmax(0, 1fr) auto;
+ min-width: 0;
+ align-items: center;
+ gap: 8px;
+}
+
+.message-footer-timing .message-activity-meta {
+ justify-content: flex-start;
+}
+
+.message-footer-timing .message-duration-meta {
+ justify-content: flex-end;
+ text-align: right;
+}
+
.message-detail-button {
display: inline-flex;
width: 22px;
diff --git a/web/hwlab-cloud-web/tests/workbench-e2e/specs/message-timing.spec.ts b/web/hwlab-cloud-web/tests/workbench-e2e/specs/message-timing.spec.ts
index 062a710b..db2dbf0f 100644
--- a/web/hwlab-cloud-web/tests/workbench-e2e/specs/message-timing.spec.ts
+++ b/web/hwlab-cloud-web/tests/workbench-e2e/specs/message-timing.spec.ts
@@ -8,10 +8,10 @@ test("Code Agent message headers render canonical timing metadata", async ({ pag
await gotoWorkbench(page, "/workbench/sessions/ses_running");
const running = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`).first();
- await expect(running.locator(`.message-header-main ${selectors.messageDurationMeta}`)).toContainText("耗时 4 秒");
- await expect(running.locator(`.message-header-actions ${selectors.messageActivityMeta}`)).toContainText("最近 0 秒前");
+ await expect(running.locator(`.message-meta-footer ${selectors.messageDurationMeta}`)).toContainText("耗时 4 秒");
+ await expect(running.locator(`.message-meta-footer ${selectors.messageActivityMeta}`)).toContainText("最近更新:0 秒前");
await expect(running.locator(selectors.messageDurationMeta)).toHaveAttribute("aria-label", "总耗时:4 秒");
- await expect(running.locator(selectors.messageActivityMeta)).toHaveAttribute("aria-label", "最近事件:0 秒前");
+ await expect(running.locator(selectors.messageActivityMeta)).toHaveAttribute("aria-label", "最近更新:0 秒前");
await expect(running.locator(selectors.messageDurationMeta)).not.toContainText("最近");
await page.clock.fastForward(5000);