diff --git a/src/renderer/components/sidebar/SidebarTaskItem.tsx b/src/renderer/components/sidebar/SidebarTaskItem.tsx index f57b9ac2..2b5c1a12 100644 --- a/src/renderer/components/sidebar/SidebarTaskItem.tsx +++ b/src/renderer/components/sidebar/SidebarTaskItem.tsx @@ -94,6 +94,7 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({ getDisplaySubject, }: SidebarTaskItemProps): React.JSX.Element { const { t } = useAppTranslation('team'); + const { t: tCommon } = useAppTranslation('common'); const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail); const teamMembers = useStore(useShallow((s) => s.teamByName[task.teamName]?.members)); const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments); @@ -137,10 +138,10 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({ ); const updatedLabel = formatUpdatedLabel( task, - t('tasks.date.updatedPrefix'), - t('tasks.date.updatedYesterday') + tCommon('tasks.date.updatedPrefix'), + tCommon('tasks.date.updatedYesterday') ); - const dateLabel = updatedLabel ?? formatTaskDate(task.createdAt, t('tasks.date.yesterday')); + const dateLabel = updatedLabel ?? formatTaskDate(task.createdAt, tCommon('tasks.date.yesterday')); const ownerColorSet = useMemo(() => { if (!teamMembers || !task.owner) return null; @@ -246,7 +247,7 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({ - {t('tasks.reviewState.needsFix')} + {tCommon('tasks.reviewState.needsFix')} )} diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 1d116114..9fc75c6a 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -522,10 +522,8 @@ export function __getTeamScopedTransientStateForTests(teamName: string): { getResolvedMemberSelectorCacheSnapshotForTeam(teamName); return { - hasResolvedMembersSelector: - resolvedMemberSelectorCacheSnapshot.hasResolvedMembersSelector, - resolvedMemberSelectorCount: - resolvedMemberSelectorCacheSnapshot.resolvedMemberSelectorCount, + hasResolvedMembersSelector: resolvedMemberSelectorCacheSnapshot.hasResolvedMembersSelector, + resolvedMemberSelectorCount: resolvedMemberSelectorCacheSnapshot.resolvedMemberSelectorCount, hasMergedMessagesSelector: messageSelectorCache.hasMergedMessagesSelector, memberMessagesSelectorCount: messageSelectorCache.memberMessagesSelectorCount, hasPendingFreshTeamDataRefresh: pendingFreshTeamDataRefreshes.has(teamName), @@ -621,12 +619,7 @@ function maybeLogMemberSpawnUiEqualSuppressed( teamName: string, runId: string | null | undefined ): void { - if ( - !shouldLogMemberSpawnUiEqualSuppressed( - teamName, - MEMBER_SPAWN_UI_EQUAL_WARN_THROTTLE_MS - ) - ) { + if (!shouldLogMemberSpawnUiEqualSuppressed(teamName, MEMBER_SPAWN_UI_EQUAL_WARN_THROTTLE_MS)) { return; } logger.debug( @@ -2050,6 +2043,13 @@ export const createTeamSlice: StateCreator = (set, let changed = false; const nextTasks = teamData.tasks.map((task) => { const nextPresence = presenceByTaskId[task.id] ?? 'unknown'; + if ( + nextPresence === 'unknown' && + task.changePresence && + task.changePresence !== 'unknown' + ) { + return task; + } if (task.changePresence === nextPresence) { return task; } diff --git a/test/renderer/components/sidebar/SidebarTaskItem.test.ts b/test/renderer/components/sidebar/SidebarTaskItem.test.ts index df2a662f..0211d209 100644 --- a/test/renderer/components/sidebar/SidebarTaskItem.test.ts +++ b/test/renderer/components/sidebar/SidebarTaskItem.test.ts @@ -1,5 +1,6 @@ import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; + import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { GlobalTask } from '../../../../src/shared/types'; @@ -29,6 +30,28 @@ vi.mock('../../../../src/renderer/hooks/useTheme', () => ({ }), })); +vi.mock('@features/localization/renderer', () => ({ + useAppTranslation: (namespace: string) => { + const catalogs: Record> = { + common: { + 'tasks.date.updatedPrefix': 'upd', + 'tasks.date.updatedYesterday': 'upd yesterday', + 'tasks.date.yesterday': 'Yesterday', + 'tasks.reviewState.needsFix': 'Needs Fixes', + }, + team: { + 'tasks.teamPrefix': 'Team:', + 'tasks.unassigned': 'Unassigned', + }, + }; + + return { + resolvedLanguage: 'en', + t: (key: string) => catalogs[namespace]?.[key] ?? key, + }; + }, +})); + vi.mock('../../../../src/renderer/components/ui/tooltip', () => ({ Tooltip: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children), @@ -203,4 +226,38 @@ describe('SidebarTaskItem unread styling', () => { await Promise.resolve(); }); }); + + it('renders translated updated and review labels instead of i18n keys', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + const updatedAt = new Date(); + const createdAt = new Date(updatedAt.getTime() - 5 * 60_000); + + await act(async () => { + root.render( + React.createElement(SidebarTaskItem, { + task: makeTask({ + createdAt: createdAt.toISOString(), + reviewState: 'needsFix', + updatedAt: updatedAt.toISOString(), + }), + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('upd'); + expect(host.textContent).toContain('Needs Fixes'); + expect(host.textContent).not.toContain('tasks.date.updatedPrefix'); + expect(host.textContent).not.toContain('tasks.reviewState.needsFix'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); }); diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts index da42c5cc..7351801c 100644 --- a/test/renderer/store/teamSlice.test.ts +++ b/test/renderer/store/teamSlice.test.ts @@ -6,8 +6,8 @@ import { __resetTeamSliceModuleStateForTests, createTeamSlice, getActiveTeamPendingReplyWaits, - hasActiveTeamPendingReplyWait, getCurrentProvisioningProgressForTeam, + hasActiveTeamPendingReplyWait, loadPersistedMessagesPanelMode, savePersistedMessagesPanelMode, selectMemberMessagesForTeamMember, @@ -24,6 +24,7 @@ import { const hoisted = vi.hoisted(() => ({ list: vi.fn(), getData: vi.fn(), + getTaskChangePresence: vi.fn(), getMessagesPage: vi.fn(), getMemberActivityMeta: vi.fn(), createTeam: vi.fn(), @@ -61,6 +62,7 @@ vi.mock('@renderer/api', () => ({ teams: { list: hoisted.list, getData: hoisted.getData, + getTaskChangePresence: hoisted.getTaskChangePresence, getMessagesPage: hoisted.getMessagesPage, getMemberActivityMeta: hoisted.getMemberActivityMeta, createTeam: hoisted.createTeam, @@ -302,6 +304,7 @@ describe('teamSlice actions', () => { __resetTeamRefreshFanoutDiagnosticsForTests(); hoisted.list.mockResolvedValue([]); hoisted.getData.mockResolvedValue(createTeamSnapshot()); + hoisted.getTaskChangePresence.mockResolvedValue({}); hoisted.getMessagesPage.mockResolvedValue({ messages: [], nextCursor: null, @@ -5147,6 +5150,36 @@ describe('teamSlice actions', () => { expect(store.getState().selectedTeamData?.tasks[0]?.changePresence).toBe('has_changes'); }); + + it('does not clear known task changePresence when presence refresh returns unknown', async () => { + const store = createSliceStore(); + store.setState({ + selectedTeamName: 'my-team', + selectedTeamData: createTeamSnapshot({ + tasks: [ + { + id: 'task-1', + subject: 'Known changes', + status: 'in_progress', + owner: 'alice', + createdAt: '2026-03-01T10:00:00.000Z', + updatedAt: '2026-03-01T10:00:00.000Z', + workIntervals: [{ startedAt: '2026-03-01T10:05:00.000Z' }], + historyEvents: [], + comments: [], + attachments: [], + changePresence: 'has_changes', + }, + ], + }), + }); + + hoisted.getTaskChangePresence.mockResolvedValue({ 'task-1': 'unknown' }); + + await store.getState().refreshTeamChangePresence('my-team'); + + expect(store.getState().selectedTeamData?.tasks[0]?.changePresence).toBe('has_changes'); + }); }); describe('provisioning run scoping', () => {