diff --git a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue index 68b902bd..5fd38bf5 100644 --- a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue +++ b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue @@ -28,14 +28,6 @@ const { following, keepBottomAfterUpdate, onScroll: onPanelScroll, scrollToBotto const detailMessageId = ref(null); const displayedDurationFloorMs = new Map(); const sealedDurationMs = new Map(); -type VisibleTimingSample = { - durationMs?: number; - durationObservedAtMs?: number; - lastEventAgeMs?: number; - lastEventObservedAtMs?: number; - lastEventSourceMs?: number; -}; -const visibleTimingSamples = new Map(); const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages); const hasRunningAgentMessages = computed(() => visibleMessages.value.some(isRunningMessage)); const { nowMs } = useWorkbenchNowTicker(hasRunningAgentMessages); @@ -74,9 +66,6 @@ watch(visibleMessages, (messages) => { for (const key of Array.from(sealedDurationMs.keys())) { if (!visibleKeys.has(key)) sealedDurationMs.delete(key); } - for (const key of Array.from(visibleTimingSamples.keys())) { - if (!visibleKeys.has(key)) visibleTimingSamples.delete(key); - } }, { immediate: true }); function openDetails(message: ChatMessage): void { @@ -280,12 +269,8 @@ function messageDurationMeta(message: ChatMessage): { text: string; label: strin function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null { if (durationMs === null) return null; const key = messageDurationFloorKey(message); - const sample = visibleTimingSamples.get(key) ?? {}; - const previous = sample.durationMs ?? displayedDurationFloorMs.get(key); - const next = smoothVisibleIncreasingDurationMs(previous, sample.durationObservedAtMs, durationMs, nowMs.value); - sample.durationMs = next; - sample.durationObservedAtMs = nowMs.value; - visibleTimingSamples.set(key, sample); + const previous = displayedDurationFloorMs.get(key); + const next = previous === undefined ? durationMs : Math.max(previous, durationMs); if (next !== previous) displayedDurationFloorMs.set(key, next); return next; } @@ -296,21 +281,12 @@ function sealedDisplayDurationMs(message: ChatMessage, durationMs: number | null const sealedFloor = sealedDurationMs.get(key); const previous = maxDefinedDurationMs(runningFloor, sealedFloor); displayedDurationFloorMs.delete(key); - visibleTimingSamples.delete(key); if (durationMs === null) return previous ?? null; const next = previous === null ? durationMs : Math.max(previous, durationMs); if (next !== previous) sealedDurationMs.set(key, next); return next; } -function smoothVisibleIncreasingDurationMs(previous: number | undefined, previousObservedAtMs: number | undefined, targetMs: number, observedAtMs: number): number { - const target = Math.max(0, Math.trunc(targetMs)); - if (previous === undefined || !Number.isFinite(previous)) return target; - const elapsedSincePreviousSample = Number.isFinite(previousObservedAtMs) ? Math.max(0, observedAtMs - Number(previousObservedAtMs)) : 0; - const visibleCeiling = previous + elapsedSincePreviousSample; - return Math.max(previous, Math.min(target, Math.trunc(visibleCeiling))); -} - function maxDefinedDurationMs(...values: Array): number | null { let maximum: number | null = null; for (const value of values) { @@ -343,34 +319,12 @@ function migrateVisibleTimingAliases(primary: string, aliases: unknown[]): void function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null { if (!isRunningMessage(message)) return null; const timing = messageTimingForDisplay(message); - const ageMs = visibleLastEventAgeMs(message, timing?.lastEventAt); + const ageMs = durationSince(timing?.lastEventAt); if (ageMs === null) return null; const value = formatDuration(ageMs) + "前"; return { text: `最近 ${value}`, label: `最近事件:${value}` }; } -function visibleLastEventAgeMs(message: ChatMessage, timestamp: unknown): number | null { - const eventAtMs = timestampMs(timestamp); - if (eventAtMs === null) return null; - const key = messageDurationFloorKey(message); - const sample = visibleTimingSamples.get(key) ?? {}; - const now = nowMs.value; - const targetAgeMs = Math.max(0, now - eventAtMs); - let nextAgeMs: number; - if (sample.lastEventAgeMs === undefined || !Number.isFinite(sample.lastEventAgeMs)) { - nextAgeMs = targetAgeMs; - } else if (sample.lastEventSourceMs !== undefined && eventAtMs > sample.lastEventSourceMs) { - nextAgeMs = 0; - } else { - nextAgeMs = smoothVisibleIncreasingDurationMs(sample.lastEventAgeMs, sample.lastEventObservedAtMs, targetAgeMs, now); - } - sample.lastEventAgeMs = nextAgeMs; - sample.lastEventObservedAtMs = now; - sample.lastEventSourceMs = eventAtMs; - visibleTimingSamples.set(key, sample); - return nextAgeMs; -} - function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] { // The main message card consumes only the durable message timing projection. // Trace timing belongs to TraceTimeline/details; letting trace hydration drive the diff --git a/web/hwlab-cloud-web/src/composables/useWorkbenchNowTicker.ts b/web/hwlab-cloud-web/src/composables/useWorkbenchNowTicker.ts index a635a92d..f8e7a93d 100644 --- a/web/hwlab-cloud-web/src/composables/useWorkbenchNowTicker.ts +++ b/web/hwlab-cloud-web/src/composables/useWorkbenchNowTicker.ts @@ -4,7 +4,6 @@ import { onBeforeUnmount, readonly, ref, watch, type Ref } from "vue"; const TICK_INTERVAL_MS = 1000; -const MAX_VISIBLE_TICK_STEP_MS = TICK_INTERVAL_MS; const nowMs = ref(Date.now()); const activeSubscribers = new Map(); @@ -31,18 +30,8 @@ export function useWorkbenchNowTicker(enabled: Ref) { return { nowMs: readonly(nowMs), refreshNow }; } -function refreshNow(reset = false): void { - const wallNow = Date.now(); - if (reset) { - nowMs.value = wallNow; - return; - } - const previous = Number(nowMs.value); - if (!Number.isFinite(previous) || wallNow <= previous) { - nowMs.value = wallNow; - return; - } - nowMs.value = Math.min(wallNow, previous + MAX_VISIBLE_TICK_STEP_MS); +function refreshNow(): void { + nowMs.value = Date.now(); } function reconcileTimer(): void { @@ -51,7 +40,7 @@ function reconcileTimer(): void { // elapsed/recent labels advance monotonically instead of jumping on resume. const shouldRun = isBrowser() && hasActiveSubscriber(); if (shouldRun && timer === null) { - refreshNow(true); + refreshNow(); timer = window.setInterval(refreshNow, TICK_INTERVAL_MS); return; }