fix: keep work-status panel reachable when all sections are hidden (#2805)

* fix: keep work-status panel reachable when all sections are hidden

When every section was unchecked in the Panel Sections dialog, the panel
went transparent and inert, making the settings gear icon unreachable.
The only recovery was knowing to toggle the panel off and on from the
header, which still rendered the same empty/inert state.

Changes:
- Panel stays interactive (not inert) when visible, even with zero
  rendered sections. This matches how other panels in the app use inert
  only for visually-collapsed (width/height = 0) states.
- Empty state shows 'No sections selected' with a link to reopen the
  sections dialog, matching the centered text-muted-foreground pattern
  used by the file tree, review panel, and home page empty states.
- Sections dialog gains a 'Show all' link (visible whenever any section
  is hidden) and a warning when all sections are unchecked, matching the
  keybinds settings 'Reset to defaults' pattern.
- Added i18n keys to all 10 locale files (English fallback).

Fixes #2804

* Round 1: fix interactive guard for fresh-mount; translate i18n keys

Address openchamber-bot review findings:

1. (blocker) Replace English fallback strings in all 10 non-English
   locale files with real translations per locale-ui-patterns guidance.

2. (non-blocker) Restore the renderedSections > 0 guard for the
   transient no-data-on-mount state so the panel doesn't flash a bare
   bordered card. The interactive condition is now:
     visible && (renderedSections > 0 || allSectionsHidden)
   Empty-state rendering is gated on allSectionsHidden alone (not
   renderedSections === 0) so it works correctly on fresh mount when
   all sections were already hidden in persisted settings.

Validation:
  tsc --noEmit: 0 errors
  bun test work-status: 34 pass, 0 fail

* Round 2: use .every() guard, dedup chooseLabel key, add tests

1. Replace >= length check with areAllWorkStatusSectionsHidden() helper
   that uses .every() — stale section ids left in persisted settings
   from a future removal can no longer inflate the count.

2. Remove duplicate chooseLabel i18n key from all 11 locales — the
   empty-state link now reuses the existing sections.open key.

3. Add 6 focused tests for areAllWorkStatusSectionsHidden covering
   empty, null/undefined, partial, full, stale-id, and stale+full.

Validation:
  tsc --noEmit: 0 errors
  bun test work-status: 40 pass (6 new), 0 fail
This commit is contained in:
Aaron Hogue
2026-08-10 23:28:50 +03:00
committed by GitHub
parent 59d988deda
commit 02872ff9d3
15 changed files with 123 additions and 7 deletions
@@ -13,7 +13,7 @@ import { WorkStatusMcpSection } from './WorkStatusMcpSection';
import { WorkStatusPinnedSection } from './WorkStatusPinnedSection';
import { WorkStatusContextSection } from './WorkStatusContextSection';
import { WorkStatusSectionsDialog } from './WorkStatusSectionsDialog';
import { isWorkStatusSectionVisible } from './sections';
import { areAllWorkStatusSectionsHidden, isWorkStatusSectionVisible } from './sections';
import { WorkStatusPresenceProvider } from './presence';
import { Icon } from '@/components/icon/Icon';
@@ -82,9 +82,14 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
// out with something in it rather than emptying first, and its subscriptions
// stop once it is truly gone.
const [contentMounted, setContentMounted] = React.useState(visible);
// Hidden, mid-collapse, or reporting nothing: in each case the card is not
// something the user can act on, so it should not be reachable.
const interactive = visible && renderedSections > 0;
// Hidden or mid-collapse: the card is not something the user can act on.
// When `visible` but all sections are hidden, the panel stays interactive so
// the settings button remains reachable — otherwise there is no way to
// re-enable sections. The previous `renderedSections > 0` guard is preserved
// for the transient "no data yet" state so the panel doesn't flash a bare
// bordered card on first mount.
const allSectionsHidden = areAllWorkStatusSectionsHidden(hiddenSections);
const interactive = visible && (renderedSections > 0 || allSectionsHidden);
React.useEffect(() => {
if (visible) {
setContentMounted(true);
@@ -180,9 +185,9 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
// separates the two without going fully opaque.
'oc-glass-panel',
],
// An empty card is a border around a settings icon, which reads as a
// fault rather than as "nothing to report".
renderedSections === 0 && 'border-transparent bg-transparent shadow-none',
// When every section is hidden the card keeps its border and background
// so the settings button stays discoverable — going transparent made the
// only recovery path unreachable.
'motion-reduce:transition-none',
'rounded-xl border border-[var(--interactive-border)]',
!overlay && 'bg-[var(--surface-muted)]/40',
@@ -246,6 +251,19 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
</WorkStatusPresenceProvider>
) : null}
{contentMounted && allSectionsHidden ? (
<div className="flex flex-col items-center justify-center px-4 py-8 text-center">
<span className="text-sm text-muted-foreground">{t('chat.workStatus.sections.allHidden')}</span>
<button
type="button"
onClick={() => setSectionsDialogOpen(true)}
className="mt-2 text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
>
{t('chat.workStatus.sections.open')}
</button>
</div>
) : null}
<WorkStatusSectionsDialog open={sectionsDialogOpen} onOpenChange={setSectionsDialogOpen} />
</aside>
);
@@ -12,6 +12,7 @@ import {
import {
WORK_STATUS_SECTION_IDS,
WORK_STATUS_SECTION_LABEL_KEYS,
areAllWorkStatusSectionsHidden,
isWorkStatusSectionVisible,
} from './sections';
@@ -29,6 +30,12 @@ export const WorkStatusSectionsDialog: React.FC<{
const { t } = useI18n();
const hidden = useUIStore((state) => state.workStatusHiddenSections);
const setSectionVisible = useUIStore((state) => state.setWorkStatusSectionVisible);
const setHiddenSections = useUIStore((state) => state.setWorkStatusHiddenSections);
const allVisible = hidden.length === 0;
const noneVisible = areAllWorkStatusSectionsHidden(hidden);
const handleShowAll = () => setHiddenSections([]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -50,6 +57,21 @@ export const WorkStatusSectionsDialog: React.FC<{
/>
))}
</div>
{!allVisible ? (
<div className="flex items-center justify-between border-t pt-3">
{noneVisible ? (
<span className="text-xs text-destructive">{t('chat.workStatus.sections.noneWarning')}</span>
) : <span />}
<button
type="button"
onClick={handleShowAll}
className="text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
>
{t('chat.workStatus.sections.showAll')}
</button>
</div>
) : null}
</DialogContent>
</Dialog>
);
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
import {
WORK_STATUS_SECTION_IDS,
WORK_STATUS_SECTION_LABEL_KEYS,
areAllWorkStatusSectionsHidden,
isWorkStatusSectionVisible,
sanitizeWorkStatusHiddenSections,
} from './sections';
@@ -30,6 +31,37 @@ describe('isWorkStatusSectionVisible', () => {
});
});
describe('areAllWorkStatusSectionsHidden', () => {
test('returns false when no sections are hidden', () => {
expect(areAllWorkStatusSectionsHidden([])).toBe(false);
});
test('returns false for null and undefined', () => {
expect(areAllWorkStatusSectionsHidden(null)).toBe(false);
expect(areAllWorkStatusSectionsHidden(undefined)).toBe(false);
});
test('returns false when only some sections are hidden', () => {
expect(areAllWorkStatusSectionsHidden(['usage', 'tasks'])).toBe(false);
});
test('returns true when every known section is hidden', () => {
expect(areAllWorkStatusSectionsHidden([...WORK_STATUS_SECTION_IDS])).toBe(true);
});
test('ignores stale ids that are no longer in the section list', () => {
// A future section-ID removal should not trick the length check into
// reporting all-hidden when real sections are still visible.
const withStale = [...WORK_STATUS_SECTION_IDS.slice(0, -1), 'removed_section'];
expect(areAllWorkStatusSectionsHidden(withStale)).toBe(false);
});
test('returns true even with extra stale ids alongside all real ones', () => {
const withExtra = [...WORK_STATUS_SECTION_IDS, 'removed_section'];
expect(areAllWorkStatusSectionsHidden(withExtra)).toBe(true);
});
});
describe('sanitizeWorkStatusHiddenSections', () => {
test('keeps known ids and drops everything else', () => {
expect(sanitizeWorkStatusHiddenSections(['usage', 'nope', 42, null, 'tasks']))
@@ -49,6 +49,17 @@ export const isWorkStatusSectionVisible = (
id: WorkStatusSectionId,
): boolean => !hidden?.includes(id);
/**
* True when every known section id appears in the hidden set.
*
* Uses `.every()` instead of a length comparison so that stale ids left over
* from a removed section cannot inflate the count past the current list length.
*/
export const areAllWorkStatusSectionsHidden = (
hidden: readonly string[] | null | undefined,
): boolean =>
hidden != null && WORK_STATUS_SECTION_IDS.every((id) => hidden.includes(id));
export const sanitizeWorkStatusHiddenSections = (value: unknown): WorkStatusSectionId[] => {
if (!Array.isArray(value)) return [];
const seen = new Set<WorkStatusSectionId>();