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,9 +245,22 @@ export function AgentActivityCluster({
const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined; const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined;
const hasVisibleActivity = reasoningSteps > 0 || toolCalls > 0 || cliCount > 0 || mcpCount > 0 || fileCount > 0; const hasVisibleActivity = reasoningSteps > 0 || toolCalls > 0 || cliCount > 0 || mcpCount > 0 || fileCount > 0;
const hasOnlyFileActivity = fileCount > 0 && messages.every(messageHasOnlyFileActivity); 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 durationMs = activityDurationMs(messages, isTurnStreaming, now, turnLatencyMs);
const activityDuration = formatActivityDuration(durationMs); const activityDuration = formatActivityDuration(durationMs);
const thoughtLabel = isTurnStreaming 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", { ? t("message.activityThinkingFor", {
duration: activityDuration, duration: activityDuration,
defaultValue: "Thinking for {{duration}}", defaultValue: "Thinking for {{duration}}",
+3
View File
@@ -987,6 +987,9 @@
"activityThinkingFor": "Thinking for {{duration}}", "activityThinkingFor": "Thinking for {{duration}}",
"activityThought": "Thought", "activityThought": "Thought",
"activityThoughtFor": "Thought for {{duration}}", "activityThoughtFor": "Thought for {{duration}}",
"activityWorkingFor": "Working for {{duration}}",
"activityWorked": "Worked",
"activityWorkedFor": "Worked for {{duration}}",
"cliActivityRunningOne": "Using @{{name}}", "cliActivityRunningOne": "Using @{{name}}",
"cliActivityRanOne": "Used @{{name}}", "cliActivityRanOne": "Used @{{name}}",
"cliActivityFailedOne": "Failed @{{name}}", "cliActivityFailedOne": "Failed @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Pensando durante {{duration}}", "activityThinkingFor": "Pensando durante {{duration}}",
"activityThought": "Pensamiento completado", "activityThought": "Pensamiento completado",
"activityThoughtFor": "Pensó durante {{duration}}", "activityThoughtFor": "Pensó durante {{duration}}",
"activityWorkingFor": "Trabajando durante {{duration}}",
"activityWorked": "Trabajo completado",
"activityWorkedFor": "Trabajó durante {{duration}}",
"cliActivityRunningOne": "Usando @{{name}}", "cliActivityRunningOne": "Usando @{{name}}",
"cliActivityRanOne": "Usó @{{name}}", "cliActivityRanOne": "Usó @{{name}}",
"cliActivityFailedOne": "Falló @{{name}}", "cliActivityFailedOne": "Falló @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Réflexion pendant {{duration}}", "activityThinkingFor": "Réflexion pendant {{duration}}",
"activityThought": "Réflexion terminée", "activityThought": "Réflexion terminée",
"activityThoughtFor": "Réflexion terminée en {{duration}}", "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}}", "cliActivityRunningOne": "Utilisation de @{{name}}",
"cliActivityRanOne": "@{{name}} utilisé", "cliActivityRanOne": "@{{name}} utilisé",
"cliActivityFailedOne": "Échec de @{{name}}", "cliActivityFailedOne": "Échec de @{{name}}",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Berpikir selama {{duration}}", "activityThinkingFor": "Berpikir selama {{duration}}",
"activityThought": "Selesai berpikir", "activityThought": "Selesai berpikir",
"activityThoughtFor": "Selesai berpikir dalam {{duration}}", "activityThoughtFor": "Selesai berpikir dalam {{duration}}",
"activityWorkingFor": "Memproses selama {{duration}}",
"activityWorked": "Selesai memproses",
"activityWorkedFor": "Diproses selama {{duration}}",
"cliActivityRunningOne": "Menggunakan @{{name}}", "cliActivityRunningOne": "Menggunakan @{{name}}",
"cliActivityRanOne": "Menggunakan @{{name}} selesai", "cliActivityRanOne": "Menggunakan @{{name}} selesai",
"cliActivityFailedOne": "@{{name}} gagal", "cliActivityFailedOne": "@{{name}} gagal",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "{{duration}}考えています", "activityThinkingFor": "{{duration}}考えています",
"activityThought": "思考しました", "activityThought": "思考しました",
"activityThoughtFor": "{{duration}}考えました", "activityThoughtFor": "{{duration}}考えました",
"activityWorkingFor": "{{duration}}作業中",
"activityWorked": "作業しました",
"activityWorkedFor": "{{duration}}作業しました",
"cliActivityRunningOne": "@{{name}} を使用中", "cliActivityRunningOne": "@{{name}} を使用中",
"cliActivityRanOne": "@{{name}} を使用しました", "cliActivityRanOne": "@{{name}} を使用しました",
"cliActivityFailedOne": "@{{name}} が失敗しました", "cliActivityFailedOne": "@{{name}} が失敗しました",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "{{duration}} 동안 생각 중", "activityThinkingFor": "{{duration}} 동안 생각 중",
"activityThought": "생각함", "activityThought": "생각함",
"activityThoughtFor": "{{duration}} 동안 생각함", "activityThoughtFor": "{{duration}} 동안 생각함",
"activityWorkingFor": "{{duration}} 동안 작업 중",
"activityWorked": "작업함",
"activityWorkedFor": "{{duration}} 동안 작업함",
"cliActivityRunningOne": "@{{name}} 사용 중", "cliActivityRunningOne": "@{{name}} 사용 중",
"cliActivityRanOne": "@{{name}} 사용함", "cliActivityRanOne": "@{{name}} 사용함",
"cliActivityFailedOne": "@{{name}} 실패", "cliActivityFailedOne": "@{{name}} 실패",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "Đang suy nghĩ trong {{duration}}", "activityThinkingFor": "Đang suy nghĩ trong {{duration}}",
"activityThought": "Đã suy nghĩ", "activityThought": "Đã suy nghĩ",
"activityThoughtFor": "Đã suy nghĩ trong {{duration}}", "activityThoughtFor": "Đã suy nghĩ trong {{duration}}",
"activityWorkingFor": "Đang xử lý trong {{duration}}",
"activityWorked": "Đã xử lý",
"activityWorkedFor": "Đã xử lý trong {{duration}}",
"cliActivityRunningOne": "Đang dùng @{{name}}", "cliActivityRunningOne": "Đang dùng @{{name}}",
"cliActivityRanOne": "Đã dùng @{{name}}", "cliActivityRanOne": "Đã dùng @{{name}}",
"cliActivityFailedOne": "@{{name}} thất bại", "cliActivityFailedOne": "@{{name}} thất bại",
+3
View File
@@ -987,6 +987,9 @@
"activityThinkingFor": "思考中 {{duration}}", "activityThinkingFor": "思考中 {{duration}}",
"activityThought": "已思考", "activityThought": "已思考",
"activityThoughtFor": "思考了 {{duration}}", "activityThoughtFor": "思考了 {{duration}}",
"activityWorkingFor": "处理中 {{duration}}",
"activityWorked": "已处理",
"activityWorkedFor": "处理了 {{duration}}",
"cliActivityRunningOne": "正在使用 @{{name}}", "cliActivityRunningOne": "正在使用 @{{name}}",
"cliActivityRanOne": "已使用 @{{name}}", "cliActivityRanOne": "已使用 @{{name}}",
"cliActivityFailedOne": "使用 @{{name}} 失败", "cliActivityFailedOne": "使用 @{{name}} 失败",
+3
View File
@@ -992,6 +992,9 @@
"activityThinkingFor": "思考中,已 {{duration}}", "activityThinkingFor": "思考中,已 {{duration}}",
"activityThought": "已思考", "activityThought": "已思考",
"activityThoughtFor": "已思考 {{duration}}", "activityThoughtFor": "已思考 {{duration}}",
"activityWorkingFor": "處理中,已 {{duration}}",
"activityWorked": "已處理",
"activityWorkedFor": "已處理 {{duration}}",
"cliActivityRunningOne": "正在使用 @{{name}}", "cliActivityRunningOne": "正在使用 @{{name}}",
"cliActivityRanOne": "已使用 @{{name}}", "cliActivityRanOne": "已使用 @{{name}}",
"cliActivityFailedOne": "@{{name}} 失敗", "cliActivityFailedOne": "@{{name}} 失敗",
+4 -4
View File
@@ -308,14 +308,14 @@ function activityItemsForMessage(message: UIMessage): ActivityItem[] {
} }
function activityTurnLatencyMs(activityMessages: UIMessage[], visibleMessages: UIMessage[]): number | undefined { 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) { for (let i = visibleMessages.length - 1; i >= 0; i -= 1) {
const latency = visibleMessages[i].latencyMs; const latency = visibleMessages[i].latencyMs;
if (isValidLatency(latency)) return latency; 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; return undefined;
} }
@@ -359,6 +359,20 @@ describe("AgentActivityCluster", () => {
expect(screen.getByText("Thought for 12s")).toBeInTheDocument(); 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", () => { it("omits the duration when completed history has no reliable timing", () => {
render( render(
<AgentActivityCluster <AgentActivityCluster
+40 -3
View File
@@ -308,10 +308,47 @@ describe("ThreadMessages", () => {
render(<ThreadMessages messages={messages} isStreaming={false} />); render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.queryByRole("button", { name: /^thinking$/i })).not.toBeInTheDocument(); 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(); 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", () => { it("keeps late activity after the live assistant answer while streaming", () => {
const messages: UIMessage[] = [ const messages: UIMessage[] = [
{ {
@@ -626,8 +663,8 @@ describe("ThreadMessages", () => {
render(<ThreadMessages messages={messages} isStreaming={false} />); render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.getByText("Thought for 15s")).toBeInTheDocument(); expect(screen.getByText("Worked for 15s")).toBeInTheDocument();
expect(screen.queryByText("Thought for 0s")).not.toBeInTheDocument(); expect(screen.queryByText("Worked for 0s")).not.toBeInTheDocument();
}); });
it("shows copy only on the last assistant slice before the next user turn", () => { it("shows copy only on the last assistant slice before the next user turn", () => {