diff --git a/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx b/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx index 5315a867..260051a2 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx @@ -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 = ({ 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 = ({ 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 = ({ sessionId, directory, visible ) : null} + {contentMounted && allSectionsHidden ? ( +
+ {t('chat.workStatus.sections.allHidden')} + +
+ ) : null} + ); diff --git a/packages/ui/src/components/chat/work-status/WorkStatusSectionsDialog.tsx b/packages/ui/src/components/chat/work-status/WorkStatusSectionsDialog.tsx index a2370c02..ad60fd4d 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusSectionsDialog.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusSectionsDialog.tsx @@ -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 ( @@ -50,6 +57,21 @@ export const WorkStatusSectionsDialog: React.FC<{ /> ))} + + {!allVisible ? ( +
+ {noneVisible ? ( + {t('chat.workStatus.sections.noneWarning')} + ) : } + +
+ ) : null}
); diff --git a/packages/ui/src/components/chat/work-status/sections.test.ts b/packages/ui/src/components/chat/work-status/sections.test.ts index 015f79ea..d4aae75c 100644 --- a/packages/ui/src/components/chat/work-status/sections.test.ts +++ b/packages/ui/src/components/chat/work-status/sections.test.ts @@ -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'])) diff --git a/packages/ui/src/components/chat/work-status/sections.ts b/packages/ui/src/components/chat/work-status/sections.ts index 358d4338..d7c6fcaf 100644 --- a/packages/ui/src/components/chat/work-status/sections.ts +++ b/packages/ui/src/components/chat/work-status/sections.ts @@ -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(); diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 507defa2..5e9bf075 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -3001,6 +3001,9 @@ export const dict = { 'chat.workStatus.sections.open': 'Abschnitte wählen', 'chat.workStatus.sections.dialogTitle': 'Panel-Abschnitte', 'chat.workStatus.sections.dialogDescription': 'Wähle, was das Arbeitsstatus-Panel zeigt. Ausgeblendete Abschnitte behalten ihre Daten und werden nur nicht angezeigt.', + 'chat.workStatus.sections.allHidden': 'Keine Abschnitte ausgewählt', + 'chat.workStatus.sections.showAll': 'Alle anzeigen', + 'chat.workStatus.sections.noneWarning': 'Das Panel wird leer angezeigt.', 'header.workStatusPanel.toggleAria': 'Arbeitsstatus-Panel umschalten', 'header.workStatusPanel.hide': 'Arbeitsstatus ausblenden', 'header.workStatusPanel.show': 'Arbeitsstatus anzeigen', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index ccbd2ad0..4b445c3f 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -3003,6 +3003,9 @@ export const dict = { 'chat.workStatus.sections.open': 'Choose sections', 'chat.workStatus.sections.dialogTitle': 'Panel sections', 'chat.workStatus.sections.dialogDescription': 'Choose what the work-status panel shows. Hidden sections keep their data — they are only left out of the panel.', + 'chat.workStatus.sections.allHidden': 'No sections selected', + 'chat.workStatus.sections.showAll': 'Show all', + 'chat.workStatus.sections.noneWarning': 'The panel will appear empty.', 'header.workStatusPanel.toggleAria': 'Toggle work-status panel', 'header.workStatusPanel.hide': 'Hide work status', 'header.workStatusPanel.show': 'Show work status', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index f88cfe0c..80f3a3d1 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -3004,6 +3004,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': 'Elegir secciones', 'chat.workStatus.sections.dialogTitle': 'Secciones del panel', 'chat.workStatus.sections.dialogDescription': 'Elige qué muestra el panel de estado. Las secciones ocultas conservan sus datos: solo no aparecen en el panel.', + 'chat.workStatus.sections.allHidden': 'Ninguna sección seleccionada', + 'chat.workStatus.sections.showAll': 'Mostrar todas', + 'chat.workStatus.sections.noneWarning': 'El panel aparecerá vacío.', 'header.workStatusPanel.toggleAria': 'Alternar panel de estado', 'header.workStatusPanel.hide': 'Ocultar estado del trabajo', 'header.workStatusPanel.show': 'Mostrar estado del trabajo', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 51f6cd8a..3d772fd3 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -3001,6 +3001,9 @@ export const dict = { 'chat.workStatus.sections.open': 'Choisir les sections', 'chat.workStatus.sections.dialogTitle': 'Sections du panneau', 'chat.workStatus.sections.dialogDescription': 'Choisis ce qu\'affiche le panneau d\'état. Les sections masquées conservent leurs données, elles sont seulement absentes du panneau.', + 'chat.workStatus.sections.allHidden': 'Aucune section sélectionnée', + 'chat.workStatus.sections.showAll': 'Tout afficher', + 'chat.workStatus.sections.noneWarning': 'Le panneau apparaîtra vide.', 'header.workStatusPanel.toggleAria': 'Basculer le panneau d\'état', 'header.workStatusPanel.hide': 'Masquer l\'état du travail', 'header.workStatusPanel.show': 'Afficher l\'état du travail', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index fa41fdd3..c386fe7f 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -3003,6 +3003,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': 'セクションを選択', 'chat.workStatus.sections.dialogTitle': 'パネルのセクション', 'chat.workStatus.sections.dialogDescription': '作業状況パネルに表示する内容を選びます。非表示のセクションもデータは保持され、表示されないだけです。', + 'chat.workStatus.sections.allHidden': 'セクションが選択されていません', + 'chat.workStatus.sections.showAll': 'すべて表示', + 'chat.workStatus.sections.noneWarning': 'パネルが空で表示されます。', 'header.workStatusPanel.toggleAria': '作業状況パネルを切り替え', 'header.workStatusPanel.hide': '作業状況を非表示', 'header.workStatusPanel.show': '作業状況を表示', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 924ae307..43c19e01 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -3003,6 +3003,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': '섹션 선택', 'chat.workStatus.sections.dialogTitle': '패널 섹션', 'chat.workStatus.sections.dialogDescription': '작업 상태 패널에 표시할 항목을 선택하세요. 숨긴 섹션도 데이터는 유지되며 패널에만 나타나지 않습니다.', + 'chat.workStatus.sections.allHidden': '선택된 섹션 없음', + 'chat.workStatus.sections.showAll': '모두 표시', + 'chat.workStatus.sections.noneWarning': '패널이 비어 보입니다.', 'header.workStatusPanel.toggleAria': '작업 상태 패널 전환', 'header.workStatusPanel.hide': '작업 상태 숨기기', 'header.workStatusPanel.show': '작업 상태 표시', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 079153ef..dd2ebb9c 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -3020,6 +3020,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': 'Wybierz sekcje', 'chat.workStatus.sections.dialogTitle': 'Sekcje panelu', 'chat.workStatus.sections.dialogDescription': 'Wybierz, co pokazuje panel stanu pracy. Ukryte sekcje zachowują swoje dane — po prostu nie są wyświetlane.', + 'chat.workStatus.sections.allHidden': 'Nie wybrano żadnych sekcji', + 'chat.workStatus.sections.showAll': 'Pokaż wszystkie', + 'chat.workStatus.sections.noneWarning': 'Panel będzie wyświetlany jako pusty.', 'header.workStatusPanel.toggleAria': 'Przełącz panel stanu pracy', 'header.workStatusPanel.hide': 'Ukryj stan pracy', 'header.workStatusPanel.show': 'Pokaż stan pracy', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 1a5cc0cf..b7f3ed07 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -3004,6 +3004,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': 'Escolher seções', 'chat.workStatus.sections.dialogTitle': 'Seções do painel', 'chat.workStatus.sections.dialogDescription': 'Escolha o que o painel de status mostra. Seções ocultas mantêm seus dados — apenas não aparecem no painel.', + 'chat.workStatus.sections.allHidden': 'Nenhuma seção selecionada', + 'chat.workStatus.sections.showAll': 'Mostrar todas', + 'chat.workStatus.sections.noneWarning': 'O painel aparecerá vazio.', 'header.workStatusPanel.toggleAria': 'Alternar painel de status', 'header.workStatusPanel.hide': 'Ocultar status do trabalho', 'header.workStatusPanel.show': 'Mostrar status do trabalho', diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 9068e465..e83f461b 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -3004,6 +3004,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': 'Обрати секції', 'chat.workStatus.sections.dialogTitle': 'Секції панелі', 'chat.workStatus.sections.dialogDescription': 'Обери, що показує панель стану роботи. Приховані секції зберігають свої дані — вони просто не відображаються.', + 'chat.workStatus.sections.allHidden': 'Жодної секції не вибрано', + 'chat.workStatus.sections.showAll': 'Показати всі', + 'chat.workStatus.sections.noneWarning': 'Панель відображатиметься порожньою.', 'header.workStatusPanel.toggleAria': 'Перемкнути панель стану роботи', 'header.workStatusPanel.hide': 'Сховати стан роботи', 'header.workStatusPanel.show': 'Показати стан роботи', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 79fbe910..04258b79 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -3004,6 +3004,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': '选择板块', 'chat.workStatus.sections.dialogTitle': '面板板块', 'chat.workStatus.sections.dialogDescription': '选择工作状态面板显示的内容。隐藏的板块仍保留数据,只是不再显示。', + 'chat.workStatus.sections.allHidden': '未选择任何部分', + 'chat.workStatus.sections.showAll': '全部显示', + 'chat.workStatus.sections.noneWarning': '面板将显示为空。', 'header.workStatusPanel.toggleAria': '切换工作状态面板', 'header.workStatusPanel.hide': '隐藏工作状态', 'header.workStatusPanel.show': '显示工作状态', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index bfbb98d0..f56bef7e 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -3003,6 +3003,9 @@ export const dict: Record = { 'chat.workStatus.sections.open': '選擇區塊', 'chat.workStatus.sections.dialogTitle': '面板區塊', 'chat.workStatus.sections.dialogDescription': '選擇工作狀態面板顯示的內容。隱藏的區塊仍保留資料,只是不再顯示。', + 'chat.workStatus.sections.allHidden': '未選擇任何部分', + 'chat.workStatus.sections.showAll': '全部顯示', + 'chat.workStatus.sections.noneWarning': '面板將顯示為空。', 'header.workStatusPanel.toggleAria': '切換工作狀態面板', 'header.workStatusPanel.hide': '隱藏工作狀態', 'header.workStatusPanel.show': '顯示工作狀態',