fix(workbench): smooth visible agent timing labels (#2005)
This commit is contained in:
@@ -28,6 +28,14 @@ const { following, keepBottomAfterUpdate, onScroll: onPanelScroll, scrollToBotto
|
|||||||
const detailMessageId = ref<string | null>(null);
|
const detailMessageId = ref<string | null>(null);
|
||||||
const displayedDurationFloorMs = new Map<string, number>();
|
const displayedDurationFloorMs = new Map<string, number>();
|
||||||
const sealedDurationMs = new Map<string, number>();
|
const sealedDurationMs = new Map<string, number>();
|
||||||
|
type VisibleTimingSample = {
|
||||||
|
durationMs?: number;
|
||||||
|
durationObservedAtMs?: number;
|
||||||
|
lastEventAgeMs?: number;
|
||||||
|
lastEventObservedAtMs?: number;
|
||||||
|
lastEventSourceMs?: number;
|
||||||
|
};
|
||||||
|
const visibleTimingSamples = new Map<string, VisibleTimingSample>();
|
||||||
const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages);
|
const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages);
|
||||||
const hasRunningAgentMessages = computed(() => visibleMessages.value.some(isRunningMessage));
|
const hasRunningAgentMessages = computed(() => visibleMessages.value.some(isRunningMessage));
|
||||||
const { nowMs } = useWorkbenchNowTicker(hasRunningAgentMessages);
|
const { nowMs } = useWorkbenchNowTicker(hasRunningAgentMessages);
|
||||||
@@ -66,6 +74,9 @@ watch(visibleMessages, (messages) => {
|
|||||||
for (const key of Array.from(sealedDurationMs.keys())) {
|
for (const key of Array.from(sealedDurationMs.keys())) {
|
||||||
if (!visibleKeys.has(key)) sealedDurationMs.delete(key);
|
if (!visibleKeys.has(key)) sealedDurationMs.delete(key);
|
||||||
}
|
}
|
||||||
|
for (const key of Array.from(visibleTimingSamples.keys())) {
|
||||||
|
if (!visibleKeys.has(key)) visibleTimingSamples.delete(key);
|
||||||
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function openDetails(message: ChatMessage): void {
|
function openDetails(message: ChatMessage): void {
|
||||||
@@ -269,8 +280,12 @@ function messageDurationMeta(message: ChatMessage): { text: string; label: strin
|
|||||||
function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null {
|
function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null {
|
||||||
if (durationMs === null) return null;
|
if (durationMs === null) return null;
|
||||||
const key = messageDurationFloorKey(message);
|
const key = messageDurationFloorKey(message);
|
||||||
const previous = displayedDurationFloorMs.get(key);
|
const sample = visibleTimingSamples.get(key) ?? {};
|
||||||
const next = previous === undefined ? durationMs : Math.max(previous, durationMs);
|
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);
|
||||||
if (next !== previous) displayedDurationFloorMs.set(key, next);
|
if (next !== previous) displayedDurationFloorMs.set(key, next);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
@@ -281,12 +296,21 @@ function sealedDisplayDurationMs(message: ChatMessage, durationMs: number | null
|
|||||||
const sealedFloor = sealedDurationMs.get(key);
|
const sealedFloor = sealedDurationMs.get(key);
|
||||||
const previous = maxDefinedDurationMs(runningFloor, sealedFloor);
|
const previous = maxDefinedDurationMs(runningFloor, sealedFloor);
|
||||||
displayedDurationFloorMs.delete(key);
|
displayedDurationFloorMs.delete(key);
|
||||||
|
visibleTimingSamples.delete(key);
|
||||||
if (durationMs === null) return previous ?? null;
|
if (durationMs === null) return previous ?? null;
|
||||||
const next = previous === null ? durationMs : Math.max(previous, durationMs);
|
const next = previous === null ? durationMs : Math.max(previous, durationMs);
|
||||||
if (next !== previous) sealedDurationMs.set(key, next);
|
if (next !== previous) sealedDurationMs.set(key, next);
|
||||||
return 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 | undefined>): number | null {
|
function maxDefinedDurationMs(...values: Array<number | undefined>): number | null {
|
||||||
let maximum: number | null = null;
|
let maximum: number | null = null;
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
@@ -303,12 +327,34 @@ function messageDurationFloorKey(message: ChatMessage): string {
|
|||||||
function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
|
function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||||
if (!isRunningMessage(message)) return null;
|
if (!isRunningMessage(message)) return null;
|
||||||
const timing = messageTimingForDisplay(message);
|
const timing = messageTimingForDisplay(message);
|
||||||
const ageMs = durationSince(timing?.lastEventAt);
|
const ageMs = visibleLastEventAgeMs(message, timing?.lastEventAt);
|
||||||
if (ageMs === null) return null;
|
if (ageMs === null) return null;
|
||||||
const value = formatDuration(ageMs) + "前";
|
const value = formatDuration(ageMs) + "前";
|
||||||
return { text: `最近 ${value}`, label: `最近事件:${value}` };
|
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"] {
|
function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
|
||||||
// The main message card consumes only the durable message timing projection.
|
// The main message card consumes only the durable message timing projection.
|
||||||
// Trace timing belongs to TraceTimeline/details; letting trace hydration drive the
|
// Trace timing belongs to TraceTimeline/details; letting trace hydration drive the
|
||||||
|
|||||||
Reference in New Issue
Block a user