From 5f7d4f5985d74ab9ea11e050d29e15565c696ddb Mon Sep 17 00:00:00 2001 From: Dorence Deng Date: Fri, 21 Aug 2026 20:53:53 +0800 Subject: [PATCH 1/2] fix(memory): format diagnostics decimal values --- .../components/MemoryDiagnosticsPanel.vue | 41 ++++++++++++++++--- .../components/MemoryDiagnosticsPanel.test.ts | 40 ++++++++++++++---- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue index a2bb2a962..6639468df 100644 --- a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue +++ b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue @@ -164,6 +164,7 @@ @@ -195,12 +196,12 @@
{{ t('settings.deepchatAgents.memoryManager.health.archivePrediction.ageDays') }}: - {{ formatNumber(lifecycle.forget.ageDays) }} + {{ formatDays(lifecycle.forget.ageDays) }} {{ t('settings.deepchatAgents.memoryManager.health.archivePrediction.decayScore') - }}: {{ formatNumber(lifecycle.forget.decayScore) }} + }}: {{ formatScore(lifecycle.forget.decayScore) }}
@@ -340,6 +341,22 @@ const props = defineProps<{ }>() const { t, te, locale } = useI18n() + +const decimalFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 3 + }) +) +const dayFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 1 + }) +) + const memoryClient = createMemoryClient() const panelFeedback = useMemoryInlineFeedback('MemoryDiagnosticsPanel') const feedback = panelFeedback.feedback @@ -384,8 +401,12 @@ const reindexFailureReason = computed(() => { return t('settings.memory.redesign.reindexInternalReason') }) const recallDiagnostics = computed(() => health.value?.runtime.agent.retrieval.recall) -const recallLatencyP50 = computed(() => recallDiagnostics.value?.latencyMs.total.p50 ?? '—') -const recallLatencyP95 = computed(() => recallDiagnostics.value?.latencyMs.total.p95 ?? '—') +const recallLatencyP50 = computed(() => + formatOptionalMilliseconds(recallDiagnostics.value?.latencyMs.total.p50) +) +const recallLatencyP95 = computed(() => + formatOptionalMilliseconds(recallDiagnostics.value?.latencyMs.total.p95) +) const fallbackCount = computed(() => { const counts = recallDiagnostics.value?.degradationCounts if (!counts) return 0 @@ -461,8 +482,16 @@ function shortId(id: string): string { return id.length > 10 ? `${id.slice(0, 4)}…${id.slice(-6)}` : id } -function formatNumber(value: number): string { - return Number.isFinite(value) ? value.toFixed(value >= 10 ? 0 : 2) : String(value) +function formatOptionalMilliseconds(value: number | null | undefined): string { + return value == null ? '—' : decimalFormatter.value.format(value) +} + +function formatScore(value: number): string { + return decimalFormatter.value.format(value) +} + +function formatDays(value: number): string { + return dayFormatter.value.format(value) } function eventLabel(eventType: string): string { diff --git a/test/renderer/components/MemoryDiagnosticsPanel.test.ts b/test/renderer/components/MemoryDiagnosticsPanel.test.ts index 9cf6144a3..21d88116b 100644 --- a/test/renderer/components/MemoryDiagnosticsPanel.test.ts +++ b/test/renderer/components/MemoryDiagnosticsPanel.test.ts @@ -180,13 +180,13 @@ describe('MemoryDiagnosticsPanel', () => { expect(clearCopy.clearConfirmBody).toContain('Standing directives are kept') }) - it('renders Agent recall and process-wide pipeline pressure', async () => { + it('renders formatted diagnostics and process-wide pipeline pressure', async () => { const health: MemoryHealthDto = structuredClone(baseHealth) health.runtime.agent.retrieval.recall.latencyMs.total = { samples: 4, - p50: 12, - p95: 48, - max: 50 + p50: 685.7158329999074, + p95: 803.200750003092, + max: 804 } health.runtime.agent.retrieval.recall.degradationCounts.vectorCold = 3 health.runtime.process.extractionQueue.depth = 2 @@ -199,18 +199,44 @@ describe('MemoryDiagnosticsPanel', () => { health.runtime.process.providerAdmission.admissionDecisions.rateLimited = 2 const providerPressureSummary = 'Rate limit {rateLimited} · Capacity {capacityRejected} · Deadline {deadline} · Aborted {aborted} · Late settle {lateSettled}' - const { wrapper, t } = await setup(baseStatus, { + const { wrapper, memoryClient, t } = await setup(baseStatus, { health, messages: { 'settings.memory.redesign.providerPressureSummary': providerPressureSummary } }) + memoryClient.getArchiveCandidateLifecyclePreview.mockResolvedValueOnce({ + ...basePreview, + lifecycles: [ + { + memoryId: 'memory-with-floating-point-diagnostics', + decayTier: 'archive_candidate', + forget: { + ageDays: 12.3456, + decayScore: 0.123456 + } + } + ] + }) + await refreshButton(wrapper).trigger('click') + await flushPromises() const pipeline = wrapper.get('[data-testid="runtime-pipeline"]') - expect(pipeline.text()).toContain('12') - expect(pipeline.text()).toContain('48') + const archiveCandidates = wrapper.get('[data-testid="archive-candidates"]') + const pipelineValues = pipeline.findAll('.tabular-nums').map((node) => node.text()) + const archiveCandidateMetrics = archiveCandidates + .findAll('.text-muted-foreground > span') + .map((node) => node.text()) + expect(pipelineValues).toContain('685.716') + expect(pipelineValues).toContain('803.201') expect(pipeline.text()).toContain('1250') expect(pipeline.text()).toContain('7') expect(pipeline.text()).toContain('Rate limit 2') expect(pipeline.text()).toContain('Deadline 1') + expect(archiveCandidateMetrics).toContain( + 'settings.deepchatAgents.memoryManager.health.archivePrediction.ageDays: 12.3' + ) + expect(archiveCandidateMetrics).toContain( + 'settings.deepchatAgents.memoryManager.health.archivePrediction.decayScore: 0.123' + ) expect(t).toHaveBeenCalledWith('settings.memory.redesign.providerPressureSummary', { rateLimited: 2, capacityRejected: 0, From 2e2d15d8a98b323447a20481a50ed181dbe8130d Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 21 Aug 2026 22:49:27 +0800 Subject: [PATCH 2/2] refactor(memory): share number formatters --- .../components/MemoryDiagnosticsPanel.vue | 35 ++++--------------- .../components/MemoryLifecyclePanel.vue | 25 ++----------- .../settings/lib/useMemoryNumberFormatters.ts | 27 ++++++++++++++ .../components/MemoryDiagnosticsPanel.test.ts | 4 +-- 4 files changed, 38 insertions(+), 53 deletions(-) create mode 100644 src/renderer/settings/lib/useMemoryNumberFormatters.ts diff --git a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue index 6639468df..2cb69d47d 100644 --- a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue +++ b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue @@ -201,7 +201,7 @@ {{ t('settings.deepchatAgents.memoryManager.health.archivePrediction.decayScore') - }}: {{ formatScore(lifecycle.forget.decayScore) }} + }}: {{ formatDecimal(lifecycle.forget.decayScore) }} @@ -333,6 +333,7 @@ import type { import { auditSentenceKey, formatRelativeTime } from './memoryRedesignUtils' import MemoryInlineFeedback from './MemoryInlineFeedback.vue' import { useMemoryInlineFeedback } from '../lib/useMemoryInlineFeedback' +import { useMemoryNumberFormatters } from '../lib/useMemoryNumberFormatters' const props = defineProps<{ agentId: string @@ -341,21 +342,7 @@ const props = defineProps<{ }>() const { t, te, locale } = useI18n() - -const decimalFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 3 - }) -) -const dayFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 1 - }) -) +const { formatDecimal, formatDays } = useMemoryNumberFormatters() const memoryClient = createMemoryClient() const panelFeedback = useMemoryInlineFeedback('MemoryDiagnosticsPanel') @@ -402,10 +389,10 @@ const reindexFailureReason = computed(() => { }) const recallDiagnostics = computed(() => health.value?.runtime.agent.retrieval.recall) const recallLatencyP50 = computed(() => - formatOptionalMilliseconds(recallDiagnostics.value?.latencyMs.total.p50) + formatOptionalDecimal(recallDiagnostics.value?.latencyMs.total.p50) ) const recallLatencyP95 = computed(() => - formatOptionalMilliseconds(recallDiagnostics.value?.latencyMs.total.p95) + formatOptionalDecimal(recallDiagnostics.value?.latencyMs.total.p95) ) const fallbackCount = computed(() => { const counts = recallDiagnostics.value?.degradationCounts @@ -482,16 +469,8 @@ function shortId(id: string): string { return id.length > 10 ? `${id.slice(0, 4)}…${id.slice(-6)}` : id } -function formatOptionalMilliseconds(value: number | null | undefined): string { - return value == null ? '—' : decimalFormatter.value.format(value) -} - -function formatScore(value: number): string { - return decimalFormatter.value.format(value) -} - -function formatDays(value: number): string { - return dayFormatter.value.format(value) +function formatOptionalDecimal(value: number | null | undefined): string { + return value == null ? '—' : formatDecimal(value) } function eventLabel(eventType: string): string { diff --git a/src/renderer/settings/components/MemoryLifecyclePanel.vue b/src/renderer/settings/components/MemoryLifecyclePanel.vue index 8e08ef5b4..dc9c3784c 100644 --- a/src/renderer/settings/components/MemoryLifecyclePanel.vue +++ b/src/renderer/settings/components/MemoryLifecyclePanel.vue @@ -151,6 +151,7 @@ import { useI18n } from 'vue-i18n' import { Icon } from '@iconify/vue' import { DcBadge } from '@dc-ui/components/badge' import type { MemoryLifecycle } from '@shared/contracts/routes' +import { useMemoryNumberFormatters } from '../lib/useMemoryNumberFormatters' const props = defineProps<{ lifecycle: MemoryLifecycle | null @@ -159,21 +160,7 @@ const props = defineProps<{ }>() const { t, locale } = useI18n() - -const decimalFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 3 - }) -) -const dayFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 1 - }) -) +const { formatDecimal: formatScore, formatDays } = useMemoryNumberFormatters() const dateFormatter = computed( () => new Intl.DateTimeFormat(locale.value || undefined, { @@ -254,20 +241,12 @@ const archiveConditions = computed(() => { ] }) -function formatScore(value: number): string { - return decimalFormatter.value.format(value) -} - function formatOptionalScore(value: number | null): string { return value === null ? t('settings.deepchatAgents.memoryManager.lifecycle.forget.notRefreshed') : formatScore(value) } -function formatDays(value: number): string { - return dayFormatter.value.format(value) -} - function formatTime(value: number): string { return dateFormatter.value.format(new Date(value)) } diff --git a/src/renderer/settings/lib/useMemoryNumberFormatters.ts b/src/renderer/settings/lib/useMemoryNumberFormatters.ts new file mode 100644 index 000000000..745a818d8 --- /dev/null +++ b/src/renderer/settings/lib/useMemoryNumberFormatters.ts @@ -0,0 +1,27 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' + +export function useMemoryNumberFormatters() { + const { locale } = useI18n() + const decimalFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 3 + }) + ) + const dayFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 1 + }) + ) + + const formatDecimal = (value: number): string => + Number.isFinite(value) ? decimalFormatter.value.format(value) : String(value) + const formatDays = (value: number): string => + Number.isFinite(value) ? dayFormatter.value.format(value) : String(value) + + return { formatDecimal, formatDays } +} diff --git a/test/renderer/components/MemoryDiagnosticsPanel.test.ts b/test/renderer/components/MemoryDiagnosticsPanel.test.ts index 21d88116b..23f3703ed 100644 --- a/test/renderer/components/MemoryDiagnosticsPanel.test.ts +++ b/test/renderer/components/MemoryDiagnosticsPanel.test.ts @@ -185,7 +185,7 @@ describe('MemoryDiagnosticsPanel', () => { health.runtime.agent.retrieval.recall.latencyMs.total = { samples: 4, p50: 685.7158329999074, - p95: 803.200750003092, + p95: Number.POSITIVE_INFINITY, max: 804 } health.runtime.agent.retrieval.recall.degradationCounts.vectorCold = 3 @@ -226,7 +226,7 @@ describe('MemoryDiagnosticsPanel', () => { .findAll('.text-muted-foreground > span') .map((node) => node.text()) expect(pipelineValues).toContain('685.716') - expect(pipelineValues).toContain('803.201') + expect(pipelineValues).toContain('Infinity') expect(pipeline.text()).toContain('1250') expect(pipeline.text()).toContain('7') expect(pipeline.text()).toContain('Rate limit 2')