fix(webui): preserve activity duration on replay
This commit is contained in:
@@ -173,6 +173,8 @@ interface AgentActivityClusterProps {
|
||||
/** True while the session turn is still running (drives “Working…” copy + header sheen). */
|
||||
isTurnStreaming: boolean;
|
||||
hasBodyBelow: boolean;
|
||||
/** Persisted end-to-end turn latency from the assistant answer, used for history replay. */
|
||||
turnLatencyMs?: number;
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
}
|
||||
@@ -185,6 +187,7 @@ export function AgentActivityCluster({
|
||||
messages,
|
||||
isTurnStreaming,
|
||||
hasBodyBelow,
|
||||
turnLatencyMs,
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
}: AgentActivityClusterProps) {
|
||||
@@ -242,12 +245,15 @@ export function AgentActivityCluster({
|
||||
const singleFilePath = fileCount === 1 ? primaryFilePath : undefined;
|
||||
const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined;
|
||||
const hasVisibleActivity = reasoningSteps > 0 || toolCalls > 0 || cliCount > 0 || mcpCount > 0 || fileCount > 0;
|
||||
const activityDuration = formatActivityDuration(activityDurationMs(messages, isTurnStreaming, now));
|
||||
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}}",
|
||||
@@ -500,7 +506,15 @@ function shortFileName(path: string): string {
|
||||
return path.split(/[\\/]/).pop() || path;
|
||||
}
|
||||
|
||||
function activityDurationMs(messages: UIMessage[], active: boolean, now: number): number {
|
||||
function activityDurationMs(
|
||||
messages: UIMessage[],
|
||||
active: boolean,
|
||||
now: number,
|
||||
completedLatencyMs?: number,
|
||||
): number {
|
||||
if (!active && Number.isFinite(completedLatencyMs) && completedLatencyMs! >= 0) {
|
||||
return Math.round(completedLatencyMs!);
|
||||
}
|
||||
const timestamps = messages
|
||||
.map((message) => message.createdAt)
|
||||
.filter((value) => Number.isFinite(value));
|
||||
@@ -513,7 +527,7 @@ function activityDurationMs(messages: UIMessage[], active: boolean, now: number)
|
||||
}
|
||||
|
||||
function formatActivityDuration(ms: number): string {
|
||||
const seconds = Math.max(0, Math.round(ms / 1000));
|
||||
const seconds = ms > 0 && ms < 1000 ? 1 : Math.max(0, Math.round(ms / 1000));
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const rest = seconds % 60;
|
||||
|
||||
@@ -134,6 +134,7 @@ function reasoningOnlyMessageFromAnswer(message: UIMessage): UIMessage {
|
||||
reasoningStreaming: message.reasoningStreaming,
|
||||
isStreaming: message.reasoningStreaming,
|
||||
activitySegmentId: message.activitySegmentId,
|
||||
latencyMs: message.latencyMs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,6 +205,8 @@ export function ThreadMessages({
|
||||
unit.type === "cluster"
|
||||
&& next?.type === "single"
|
||||
&& next.message.role === "assistant";
|
||||
const turnLatencyMs =
|
||||
unit.type === "cluster" ? activityClusterTurnLatencyMs(unit.messages, next) : undefined;
|
||||
|
||||
return (
|
||||
<div key={unitKey(unit, index)} className={marginTop}>
|
||||
@@ -212,6 +215,7 @@ export function ThreadMessages({
|
||||
messages={unit.messages}
|
||||
isTurnStreaming={index === liveActivityClusterIndex}
|
||||
hasBodyBelow={hasBodyBelow}
|
||||
turnLatencyMs={turnLatencyMs}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
/>
|
||||
@@ -234,6 +238,28 @@ export function ThreadMessages({
|
||||
);
|
||||
}
|
||||
|
||||
function activityClusterTurnLatencyMs(
|
||||
messages: UIMessage[],
|
||||
next: DisplayUnit | undefined,
|
||||
): number | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const latency = messages[i].latencyMs;
|
||||
if (typeof latency === "number" && Number.isFinite(latency) && latency >= 0) {
|
||||
return latency;
|
||||
}
|
||||
}
|
||||
if (
|
||||
next?.type === "single"
|
||||
&& next.message.role === "assistant"
|
||||
&& typeof next.message.latencyMs === "number"
|
||||
&& Number.isFinite(next.message.latencyMs)
|
||||
&& next.message.latencyMs >= 0
|
||||
) {
|
||||
return next.message.latencyMs;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function currentActivityClusterIndex(units: DisplayUnit[]): number {
|
||||
const last = units.length - 1;
|
||||
return units[last]?.type === "cluster" ? last : -1;
|
||||
|
||||
Reference in New Issue
Block a user