From 472c67722e458608e24f44f147cfbd7d012eb739 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Thu, 11 Jun 2026 00:15:46 +0800 Subject: [PATCH] fix(webui): correct activity duration display --- .../thread/AgentActivityCluster.tsx | 35 ++++++++++----- webui/src/i18n/locales/en/common.json | 3 ++ webui/src/i18n/locales/es/common.json | 3 ++ webui/src/i18n/locales/fr/common.json | 3 ++ webui/src/i18n/locales/id/common.json | 3 ++ webui/src/i18n/locales/ja/common.json | 3 ++ webui/src/i18n/locales/ko/common.json | 3 ++ webui/src/i18n/locales/vi/common.json | 3 ++ webui/src/i18n/locales/zh-CN/common.json | 3 ++ webui/src/i18n/locales/zh-TW/common.json | 3 ++ webui/src/lib/activity-timeline.ts | 8 ++-- .../src/tests/agent-activity-cluster.test.tsx | 14 ++++++ webui/src/tests/thread-messages.test.tsx | 43 +++++++++++++++++-- 13 files changed, 109 insertions(+), 18 deletions(-) diff --git a/webui/src/components/thread/AgentActivityCluster.tsx b/webui/src/components/thread/AgentActivityCluster.tsx index 0210b055..47e36ebc 100644 --- a/webui/src/components/thread/AgentActivityCluster.tsx +++ b/webui/src/components/thread/AgentActivityCluster.tsx @@ -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 diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index 71acd9f6..80c8bc96 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -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}}", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 6856a735..05df277e 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -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}}", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 5139d5c5..091dece5 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -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}}", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 884123b5..74dddadb 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -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", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index a1b464f1..476ab40b 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -992,6 +992,9 @@ "activityThinkingFor": "{{duration}}考えています", "activityThought": "思考しました", "activityThoughtFor": "{{duration}}考えました", + "activityWorkingFor": "{{duration}}作業中", + "activityWorked": "作業しました", + "activityWorkedFor": "{{duration}}作業しました", "cliActivityRunningOne": "@{{name}} を使用中", "cliActivityRanOne": "@{{name}} を使用しました", "cliActivityFailedOne": "@{{name}} が失敗しました", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index 0a6cdab2..bd47cd5b 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -992,6 +992,9 @@ "activityThinkingFor": "{{duration}} 동안 생각 중", "activityThought": "생각함", "activityThoughtFor": "{{duration}} 동안 생각함", + "activityWorkingFor": "{{duration}} 동안 작업 중", + "activityWorked": "작업함", + "activityWorkedFor": "{{duration}} 동안 작업함", "cliActivityRunningOne": "@{{name}} 사용 중", "cliActivityRanOne": "@{{name}} 사용함", "cliActivityFailedOne": "@{{name}} 실패", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index e5c82164..122a16d9 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -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", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index cd816680..7a5bf81f 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -987,6 +987,9 @@ "activityThinkingFor": "思考中 {{duration}}", "activityThought": "已思考", "activityThoughtFor": "思考了 {{duration}}", + "activityWorkingFor": "处理中 {{duration}}", + "activityWorked": "已处理", + "activityWorkedFor": "处理了 {{duration}}", "cliActivityRunningOne": "正在使用 @{{name}}", "cliActivityRanOne": "已使用 @{{name}}", "cliActivityFailedOne": "使用 @{{name}} 失败", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index bd5ace9c..b33139ce 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -992,6 +992,9 @@ "activityThinkingFor": "思考中,已 {{duration}}", "activityThought": "已思考", "activityThoughtFor": "已思考 {{duration}}", + "activityWorkingFor": "處理中,已 {{duration}}", + "activityWorked": "已處理", + "activityWorkedFor": "已處理 {{duration}}", "cliActivityRunningOne": "正在使用 @{{name}}", "cliActivityRanOne": "已使用 @{{name}}", "cliActivityFailedOne": "@{{name}} 失敗", diff --git a/webui/src/lib/activity-timeline.ts b/webui/src/lib/activity-timeline.ts index f1f912ce..ce1ebbcf 100644 --- a/webui/src/lib/activity-timeline.ts +++ b/webui/src/lib/activity-timeline.ts @@ -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; } diff --git a/webui/src/tests/agent-activity-cluster.test.tsx b/webui/src/tests/agent-activity-cluster.test.tsx index 5ebd0969..58449a95 100644 --- a/webui/src/tests/agent-activity-cluster.test.tsx +++ b/webui/src/tests/agent-activity-cluster.test.tsx @@ -359,6 +359,20 @@ describe("AgentActivityCluster", () => { expect(screen.getByText("Thought for 12s")).toBeInTheDocument(); }); + it("labels mixed tool activity as work instead of thought", () => { + render( + , + ); + + 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( { render(); 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(); + 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(); - 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", () => {