fix: move Workbench timing metadata below messages

This commit is contained in:
root
2026-07-20 20:37:45 +02:00
parent 28269aee0b
commit 28e6643251
4 changed files with 80 additions and 17 deletions
@@ -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, {
@@ -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"
>
<header v-if="message.role !== 'user'">
<div class="message-header-main">
<strong>{{ message.title }}</strong>
<span v-if="durationMeta" class="message-timing-meta message-duration-meta" :aria-label="durationMeta.label" :title="durationMeta.label">{{ durationMeta.text }}</span>
</div>
<div class="message-header-actions">
<span v-if="activityMeta" class="message-activity-meta" :aria-label="activityMeta.label" :title="activityMeta.label">{{ activityMeta.text }}</span>
<span v-if="running" class="agent-running-indicator" aria-label="运行中" role="status"><span aria-hidden="true" /></span>
<StatusBadge v-if="showStatusBadge(message)" :status="message.status" />
<button v-if="detailEnabled && message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="emit('details', message)">!</button>
</div>
</header>
<section v-if="semanticRuntimeStatus" class="semantic-failure-banner" :data-tone="semanticRuntimeStatus.tone" :data-emphasis="semanticRuntimeStatus.emphasis" role="status" aria-live="polite" :title="semanticRuntimeStatus.code ? `${semanticRuntimeStatus.title} · ${semanticRuntimeStatus.detail} · ${semanticRuntimeStatus.code}` : undefined">
<span class="semantic-status-marker" aria-hidden="true" />
<strong>{{ semanticRuntimeStatus.title }}</strong>
@@ -158,6 +146,20 @@ function traceStorageKey(message: ChatMessage): string {
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="启动中..." compact />
<MessageMarkdown v-if="visibleText" class="message-text" :source="visibleText" />
<ApiErrorDiagnostic v-if="diagnostic.visible" class="message-diagnostic projection-diagnostic" :error="diagnostic.text" :api-error="diagnostic.apiError" :diagnostic="diagnostic.diagnostic" :show-message="diagnostic.showMessage" compact />
<footer v-if="message.role !== 'user'" class="message-meta-footer">
<div class="message-header-main">
<strong>{{ message.title }}</strong>
</div>
<div class="message-header-actions">
<span v-if="running" class="agent-running-indicator" aria-label="运行中" role="status"><span aria-hidden="true" /></span>
<StatusBadge v-if="showStatusBadge(message)" :status="message.status" />
<button v-if="detailEnabled && message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="emit('details', message)">!</button>
</div>
<div class="message-footer-timing">
<span v-if="activityMeta" class="message-activity-meta" :aria-label="activityMeta.label" :title="activityMeta.label">{{ activityMeta.text }}</span>
<span v-if="durationMeta" class="message-timing-meta message-duration-meta" :aria-label="durationMeta.label" :title="durationMeta.label">{{ durationMeta.text }}</span>
</div>
</footer>
</article>
</template>
+27 -1
View File
@@ -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;
@@ -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);