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", () => {