diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index b979e6de..3ba54e31 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -1428,7 +1428,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project const base = { ...message, traceId: canonicalTraceId, runnerTrace, updatedAt: new Date().toISOString() }; if (message.role === "user" && userMessageId) return { ...base, id: userMessageId, messageId: userMessageId, turnId }; if (message.role === "agent") { - const next = { ...base, ...admissionTiming }; + const next = { ...base, ...stableAdmissionTimingPatchForRunningMessage(message, admissionTiming) }; return { ...next, ...(assistantMessageId ? { id: assistantMessageId, messageId: assistantMessageId } : {}), @@ -1440,6 +1440,21 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project return canonicalTraceId; } + function stableAdmissionTimingPatchForRunningMessage(message: ChatMessage, patch: Partial): Partial { + const incoming = patch.timing; + const existing = message.timing; + if (!incoming || !existing?.startedAt || !isRunningMessage(message)) return patch; + const timing = { + ...incoming, + startedAt: existing.startedAt, + lastEventAt: existing.lastEventAt ?? incoming.lastEventAt ?? existing.startedAt, + finishedAt: null, + durationMs: null, + valuesRedacted: incoming.valuesRedacted !== false, + } as WorkbenchTurnTimingProjection; + return { ...patch, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null }; + } + function optimisticAdmissionTimingPatch(lifecycle: AgentChatResponse): Partial { const patch = messageTimingPatch(lifecycle); if (patch.timing?.startedAt && patch.timing?.lastEventAt) return patch;