diff --git a/src/renderer/src/components/native-chat/NativeChatMonitoringStatus.tsx b/src/renderer/src/components/native-chat/NativeChatMonitoringStatus.tsx index 4775417bd77..e63f4ab6271 100644 --- a/src/renderer/src/components/native-chat/NativeChatMonitoringStatus.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMonitoringStatus.tsx @@ -1,6 +1,5 @@ -import { AgentStateDot, agentStateLabel } from '@/components/AgentStateDot' - -const MONITORING_LABEL = agentStateLabel('monitoring') +import { AgentStateDot } from '@/components/AgentStateDot' +import { translate } from '@/i18n/i18n' export function NativeChatMonitoringStatus({ monitoring @@ -20,7 +19,9 @@ export function NativeChatMonitoringStatus({ - {MONITORING_LABEL} + + {translate('components.native-chat.monitoringStatus.label', 'Monitoring background tasks')} + ) } diff --git a/src/renderer/src/components/native-chat/use-native-chat-hook-status.ts b/src/renderer/src/components/native-chat/use-native-chat-hook-status.ts index e2a7d87749a..12d84f4bc63 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-hook-status.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-hook-status.ts @@ -44,12 +44,11 @@ function resolveNativeChatHookStatus( } export function useNativeChatMonitoringStatus(paneKey: string): boolean { - // Why: monitoring stays out of foreground lifecycle while remaining visible from the tab glyph's pane-status source. - const agentStatusEpoch = useAppStore((store) => store.agentStatusEpoch) - void agentStatusEpoch - return useAppStore((store) => - resolveNativeChatMonitoringStatus(store.agentStatusByPaneKey[paneKey]) - ) + return useAppStore((store) => { + // Why: epoch invalidates freshness; the primitive result avoids unrelated-pane rerenders. + void store.agentStatusEpoch + return resolveNativeChatMonitoringStatus(store.agentStatusByPaneKey[paneKey]) + }) } export function useNativeChatHookStatus( diff --git a/src/renderer/src/components/native-chat/use-native-chat-monitoring-status.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-monitoring-status.test.tsx new file mode 100644 index 00000000000..4952072ad76 --- /dev/null +++ b/src/renderer/src/components/native-chat/use-native-chat-monitoring-status.test.tsx @@ -0,0 +1,66 @@ +// @vitest-environment happy-dom + +import '@testing-library/jest-dom/vitest' + +import { act, cleanup, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types' +import { useAppStore } from '@/store' +import { useNativeChatMonitoringStatus } from './use-native-chat-hook-status' + +const PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111' + +function monitoringEntry(updatedAt: number): AgentStatusEntry { + return { + paneKey: PANE_KEY, + state: 'working', + workingMode: 'monitoring', + prompt: 'Monitor background tasks', + updatedAt, + stateStartedAt: updatedAt, + stateHistory: [] + } +} + +describe('useNativeChatMonitoringStatus', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-09-04T12:00:00.000Z')) + useAppStore.setState({ + agentStatusByPaneKey: { [PANE_KEY]: monitoringEntry(Date.now()) }, + agentStatusEpoch: 0 + }) + }) + + afterEach(() => { + cleanup() + useAppStore.setState({ agentStatusByPaneKey: {}, agentStatusEpoch: 0 }) + vi.useRealTimers() + }) + + it('rerenders at expiry without rerendering for an unrelated epoch', () => { + let renders = 0 + function Probe(): React.JSX.Element { + renders += 1 + const monitoring = useNativeChatMonitoringStatus(PANE_KEY) + return {monitoring ? 'monitoring' : 'idle'} + } + + render() + expect(screen.getByText('monitoring')).toBeInTheDocument() + expect(renders).toBe(1) + + act(() => { + useAppStore.setState((state) => ({ agentStatusEpoch: state.agentStatusEpoch + 1 })) + }) + expect(renders).toBe(1) + + vi.setSystemTime(Date.now() + AGENT_STATUS_STALE_AFTER_MS + 1) + act(() => { + useAppStore.setState((state) => ({ agentStatusEpoch: state.agentStatusEpoch + 1 })) + }) + expect(screen.getByText('idle')).toBeInTheDocument() + expect(renders).toBe(2) + }) +}) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index f2da58bffb3..d9fb5b898b0 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17009,7 +17009,10 @@ }, "structuredSessionCloseFailed": "Could not close this Codex chat", "structuredSessionLaunchFailed": "Could not open Codex chat", - "structuredSessionCloseFailedDescription": "The terminal stayed open so the provider remains recoverable." + "structuredSessionCloseFailedDescription": "The terminal stayed open so the provider remains recoverable.", + "monitoringStatus": { + "label": "Monitoring background tasks" + } }, "tab": { "bar": {