fix(webui): correct activity duration display

This commit is contained in:
chengyongru
2026-06-18 00:03:18 +08:00
committed by Xubin Ren
parent b46aac0554
commit 472c67722e
13 changed files with 109 additions and 18 deletions
@@ -245,19 +245,32 @@ export function AgentActivityCluster({
const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined;
const hasVisibleActivity = reasoningSteps > 0 || toolCalls > 0 || cliCount > 0 || mcpCount > 0 || fileCount > 0;
const hasOnlyFileActivity = fileCount > 0 && messages.every(messageHasOnlyFileActivity);
const hasNonReasoningActivity = toolCalls > 0 || cliCount > 0 || mcpCount > 0 || fileCount > 0;
const durationMs = activityDurationMs(messages, isTurnStreaming, now, turnLatencyMs);
const activityDuration = formatActivityDuration(durationMs);
const thoughtLabel = isTurnStreaming
? t("message.activityThinkingFor", {
duration: activityDuration,
defaultValue: "Thinking for {{duration}}",
})
: durationMs <= 0
? t("message.activityThought", { defaultValue: "Thought" })
: t("message.activityThoughtFor", {
duration: activityDuration,
defaultValue: "Thought for {{duration}}",
});
const thoughtLabel = hasNonReasoningActivity
? isTurnStreaming
? t("message.activityWorkingFor", {
duration: activityDuration,
defaultValue: "Working for {{duration}}",
})
: durationMs <= 0
? t("message.activityWorked", { defaultValue: "Worked" })
: t("message.activityWorkedFor", {
duration: activityDuration,
defaultValue: "Worked for {{duration}}",
})
: isTurnStreaming
? t("message.activityThinkingFor", {
duration: activityDuration,
defaultValue: "Thinking for {{duration}}",
})
: durationMs <= 0
? t("message.activityThought", { defaultValue: "Thought" })
: t("message.activityThoughtFor", {
duration: activityDuration,
defaultValue: "Thought for {{duration}}",
});
const fileActivitySummary = fileCount > 0
? hasPendingFileEdit && !singleFilePath
+3
View File
@@ -987,6 +987,9 @@
"activityThinkingFor": "Thinking for {{duration}}",
"activityThought": "Thought",
"activityThoughtFor": "Thought for {{duration}}",
"activityWorkingFor": "Working for {{duration}}",
"activityWorked": "Worked",
"activityWorkedFor": "Worked for {{duration}}",
"cliActivityRunningOne": "Using @{{name}}",
"cliActivityRanOne": "Used @{{name}}",
"cliActivityFailedOne": "Failed @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Pensando durante {{duration}}",
"activityThought": "Pensamiento completado",
"activityThoughtFor": "Pensó durante {{duration}}",
"activityWorkingFor": "Trabajando durante {{duration}}",
"activityWorked": "Trabajo completado",
"activityWorkedFor": "Trabajó durante {{duration}}",
"cliActivityRunningOne": "Usando @{{name}}",
"cliActivityRanOne": "Usó @{{name}}",
"cliActivityFailedOne": "Falló @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Réflexion pendant {{duration}}",
"activityThought": "Réflexion terminée",
"activityThoughtFor": "Réflexion terminée en {{duration}}",
"activityWorkingFor": "Travail en cours depuis {{duration}}",
"activityWorked": "Travail terminé",
"activityWorkedFor": "Travail terminé en {{duration}}",
"cliActivityRunningOne": "Utilisation de @{{name}}",
"cliActivityRanOne": "@{{name}} utilisé",
"cliActivityFailedOne": "Échec de @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Berpikir selama {{duration}}",
"activityThought": "Selesai berpikir",
"activityThoughtFor": "Selesai berpikir dalam {{duration}}",
"activityWorkingFor": "Memproses selama {{duration}}",
"activityWorked": "Selesai memproses",
"activityWorkedFor": "Diproses selama {{duration}}",
"cliActivityRunningOne": "Menggunakan @{{name}}",
"cliActivityRanOne": "Menggunakan @{{name}} selesai",
"cliActivityFailedOne": "@{{name}} gagal",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "{{duration}}考えています",
"activityThought": "思考しました",
"activityThoughtFor": "{{duration}}考えました",
"activityWorkingFor": "{{duration}}作業中",
"activityWorked": "作業しました",
"activityWorkedFor": "{{duration}}作業しました",
"cliActivityRunningOne": "@{{name}} を使用中",
"cliActivityRanOne": "@{{name}} を使用しました",
"cliActivityFailedOne": "@{{name}} が失敗しました",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "{{duration}} 동안 생각 중",
"activityThought": "생각함",
"activityThoughtFor": "{{duration}} 동안 생각함",
"activityWorkingFor": "{{duration}} 동안 작업 중",
"activityWorked": "작업함",
"activityWorkedFor": "{{duration}} 동안 작업함",
"cliActivityRunningOne": "@{{name}} 사용 중",
"cliActivityRanOne": "@{{name}} 사용함",
"cliActivityFailedOne": "@{{name}} 실패",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Đang suy nghĩ trong {{duration}}",
"activityThought": "Đã suy nghĩ",
"activityThoughtFor": "Đã suy nghĩ trong {{duration}}",
"activityWorkingFor": "Đang xử lý trong {{duration}}",
"activityWorked": "Đã xử lý",
"activityWorkedFor": "Đã xử lý trong {{duration}}",
"cliActivityRunningOne": "Đang dùng @{{name}}",
"cliActivityRanOne": "Đã dùng @{{name}}",
"cliActivityFailedOne": "@{{name}} thất bại",
+3
View File
@@ -987,6 +987,9 @@
"activityThinkingFor": "思考中 {{duration}}",
"activityThought": "已思考",
"activityThoughtFor": "思考了 {{duration}}",
"activityWorkingFor": "处理中 {{duration}}",
"activityWorked": "已处理",
"activityWorkedFor": "处理了 {{duration}}",
"cliActivityRunningOne": "正在使用 @{{name}}",
"cliActivityRanOne": "已使用 @{{name}}",
"cliActivityFailedOne": "使用 @{{name}} 失败",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "思考中,已 {{duration}}",
"activityThought": "已思考",
"activityThoughtFor": "已思考 {{duration}}",
"activityWorkingFor": "處理中,已 {{duration}}",
"activityWorked": "已處理",
"activityWorkedFor": "已處理 {{duration}}",
"cliActivityRunningOne": "正在使用 @{{name}}",
"cliActivityRanOne": "已使用 @{{name}}",
"cliActivityFailedOne": "@{{name}} 失敗",
+4 -4
View File
@@ -308,14 +308,14 @@ function activityItemsForMessage(message: UIMessage): ActivityItem[] {
}
function activityTurnLatencyMs(activityMessages: UIMessage[], visibleMessages: UIMessage[]): number | undefined {
for (let i = activityMessages.length - 1; i >= 0; i -= 1) {
const latency = activityMessages[i].latencyMs;
if (isValidLatency(latency)) return latency;
}
for (let i = visibleMessages.length - 1; i >= 0; i -= 1) {
const latency = visibleMessages[i].latencyMs;
if (isValidLatency(latency)) return latency;
}
for (let i = activityMessages.length - 1; i >= 0; i -= 1) {
const latency = activityMessages[i].latencyMs;
if (isValidLatency(latency)) return latency;
}
return undefined;
}
@@ -359,6 +359,20 @@ describe("AgentActivityCluster", () => {
expect(screen.getByText("Thought for 12s")).toBeInTheDocument();
});
it("labels mixed tool activity as work instead of thought", () => {
render(
<AgentActivityCluster
messages={activityMessages()}
isTurnStreaming={false}
hasBodyBelow
turnLatencyMs={12_400}
/>,
);
expect(screen.getByText("Worked for 12s")).toBeInTheDocument();
expect(screen.queryByText("Thought for 12s")).not.toBeInTheDocument();
});
it("omits the duration when completed history has no reliable timing", () => {
render(
<AgentActivityCluster
+40 -3
View File
@@ -308,10 +308,47 @@ describe("ThreadMessages", () => {
render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.queryByRole("button", { name: /^thinking$/i })).not.toBeInTheDocument();
expect(screen.getByText("Thought for 9s")).toBeInTheDocument();
expect(screen.getByText("Worked for 9s")).toBeInTheDocument();
expect(screen.getByText("final answer")).toBeInTheDocument();
});
it("uses final turn latency when an earlier reasoning segment has its own latency", () => {
const messages: UIMessage[] = [
{
id: "r1",
role: "assistant",
content: "",
reasoning: "plan",
reasoningStreaming: false,
latencyMs: 3_000,
createdAt: 1,
},
{
id: "t1",
role: "tool",
kind: "trace",
content: "shell()",
traces: ["shell()"],
createdAt: 2,
},
{
id: "a1",
role: "assistant",
content: "done",
latencyMs: 20_000,
createdAt: 3,
},
];
const units = buildDisplayUnits(messages);
expect(units[0].type === "activity" ? units[0].turnLatencyMs : undefined).toBe(20_000);
render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.getByText("Worked for 20s")).toBeInTheDocument();
expect(screen.queryByText("Worked for 3s")).not.toBeInTheDocument();
});
it("keeps late activity after the live assistant answer while streaming", () => {
const messages: UIMessage[] = [
{
@@ -626,8 +663,8 @@ describe("ThreadMessages", () => {
render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.getByText("Thought for 15s")).toBeInTheDocument();
expect(screen.queryByText("Thought for 0s")).not.toBeInTheDocument();
expect(screen.getByText("Worked for 15s")).toBeInTheDocument();
expect(screen.queryByText("Worked for 0s")).not.toBeInTheDocument();
});
it("shows copy only on the last assistant slice before the next user turn", () => {