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', () => {