test: cover work-status empty panel recovery
This commit is contained in:
@@ -203,6 +203,11 @@ section decides for itself that it has nothing to say, so they report through
|
||||
`presenceContext.ts` and the panel collapses when none rendered. Deriving that
|
||||
at the panel level would mean duplicating every data source the sections read.
|
||||
|
||||
There is one deliberate exception: when the user hides every section, the card
|
||||
stays visible with a localized empty state and section controls. Collapsing that
|
||||
state would also hide the only recovery path. A panel with enabled sections but
|
||||
no data still follows the presence reports and collapses as before.
|
||||
|
||||
The scroll offset resets on session change: restoring one session's offset into
|
||||
another's shorter panel lands somewhere arbitrary.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { WORK_STATUS_PANEL_WIDTH } from './useWorkStatusVisibility';
|
||||
import { WorkStatusGoalRow } from './WorkStatusGoalRow';
|
||||
@@ -13,7 +14,11 @@ import { WorkStatusMcpSection } from './WorkStatusMcpSection';
|
||||
import { WorkStatusPinnedSection } from './WorkStatusPinnedSection';
|
||||
import { WorkStatusContextSection } from './WorkStatusContextSection';
|
||||
import { WorkStatusSectionsDialog } from './WorkStatusSectionsDialog';
|
||||
import { areAllWorkStatusSectionsHidden, isWorkStatusSectionVisible } from './sections';
|
||||
import {
|
||||
areAllWorkStatusSectionsHidden,
|
||||
getWorkStatusPanelPresentation,
|
||||
isWorkStatusSectionVisible,
|
||||
} from './sections';
|
||||
import { WorkStatusPresenceProvider } from './presence';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
|
||||
@@ -89,7 +94,12 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
|
||||
// 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);
|
||||
const { interactive, showEmptyState } = getWorkStatusPanelPresentation({
|
||||
visible,
|
||||
contentMounted,
|
||||
renderedSections,
|
||||
allSectionsHidden,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
setContentMounted(true);
|
||||
@@ -251,16 +261,17 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
|
||||
</WorkStatusPresenceProvider>
|
||||
) : null}
|
||||
|
||||
{contentMounted && allSectionsHidden ? (
|
||||
{showEmptyState ? (
|
||||
<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"
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
onClick={() => setSectionsDialogOpen(true)}
|
||||
className="mt-2 text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
|
||||
className="mt-2 normal-case text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t('chat.workStatus.sections.open')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -63,13 +64,14 @@ export const WorkStatusSectionsDialog: React.FC<{
|
||||
{noneVisible ? (
|
||||
<span className="text-xs text-destructive">{t('chat.workStatus.sections.noneWarning')}</span>
|
||||
) : <span />}
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
onClick={handleShowAll}
|
||||
className="text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
|
||||
className="normal-case text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t('chat.workStatus.sections.showAll')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
WORK_STATUS_SECTION_IDS,
|
||||
WORK_STATUS_SECTION_LABEL_KEYS,
|
||||
areAllWorkStatusSectionsHidden,
|
||||
getWorkStatusPanelPresentation,
|
||||
isWorkStatusSectionVisible,
|
||||
sanitizeWorkStatusHiddenSections,
|
||||
} from './sections';
|
||||
@@ -62,6 +63,44 @@ describe('areAllWorkStatusSectionsHidden', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkStatusPanelPresentation', () => {
|
||||
test('keeps a visible all-hidden panel interactive and renders its recovery state', () => {
|
||||
expect(getWorkStatusPanelPresentation({
|
||||
visible: true,
|
||||
contentMounted: true,
|
||||
renderedSections: 0,
|
||||
allSectionsHidden: true,
|
||||
})).toEqual({ interactive: true, showEmptyState: true });
|
||||
});
|
||||
|
||||
test('covers the optimistic fresh-mount count when all sections are hidden', () => {
|
||||
expect(getWorkStatusPanelPresentation({
|
||||
visible: true,
|
||||
contentMounted: true,
|
||||
renderedSections: 1,
|
||||
allSectionsHidden: true,
|
||||
})).toEqual({ interactive: true, showEmptyState: true });
|
||||
});
|
||||
|
||||
test('preserves collapse when no section has data but sections remain enabled', () => {
|
||||
expect(getWorkStatusPanelPresentation({
|
||||
visible: true,
|
||||
contentMounted: true,
|
||||
renderedSections: 0,
|
||||
allSectionsHidden: false,
|
||||
})).toEqual({ interactive: false, showEmptyState: false });
|
||||
});
|
||||
|
||||
test('does not expose controls or the empty state during a hidden collapse', () => {
|
||||
expect(getWorkStatusPanelPresentation({
|
||||
visible: false,
|
||||
contentMounted: false,
|
||||
renderedSections: 0,
|
||||
allSectionsHidden: true,
|
||||
})).toEqual({ interactive: false, showEmptyState: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeWorkStatusHiddenSections', () => {
|
||||
test('keeps known ids and drops everything else', () => {
|
||||
expect(sanitizeWorkStatusHiddenSections(['usage', 'nope', 42, null, 'tasks']))
|
||||
|
||||
@@ -60,6 +60,21 @@ export const areAllWorkStatusSectionsHidden = (
|
||||
): boolean =>
|
||||
hidden != null && WORK_STATUS_SECTION_IDS.every((id) => hidden.includes(id));
|
||||
|
||||
export const getWorkStatusPanelPresentation = ({
|
||||
visible,
|
||||
contentMounted,
|
||||
renderedSections,
|
||||
allSectionsHidden,
|
||||
}: {
|
||||
visible: boolean;
|
||||
contentMounted: boolean;
|
||||
renderedSections: number;
|
||||
allSectionsHidden: boolean;
|
||||
}): { interactive: boolean; showEmptyState: boolean } => ({
|
||||
interactive: visible && (renderedSections > 0 || allSectionsHidden),
|
||||
showEmptyState: contentMounted && allSectionsHidden,
|
||||
});
|
||||
|
||||
export const sanitizeWorkStatusHiddenSections = (value: unknown): WorkStatusSectionId[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const seen = new Set<WorkStatusSectionId>();
|
||||
|
||||
Reference in New Issue
Block a user