github-issue-tower-defence-management 1.193.0 → 1.194.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -0
  3. package/bin/adapter/entry-points/cli/index.js +19 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-RaFqjB7u.js → index-CYwLv9pf.js} +2 -2
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  7. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  8. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  9. package/bin/adapter/repositories/NodeLocalCommandRunner.js +5 -0
  10. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
  11. package/bin/adapter/repositories/NodeTmuxSessionRepository.js +29 -0
  12. package/bin/adapter/repositories/NodeTmuxSessionRepository.js.map +1 -1
  13. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +4 -2
  14. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  15. package/bin/domain/usecases/StartPreparationUseCase.js +12 -2
  16. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +1 -3
  18. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
  19. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +12 -4
  20. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/adapter/entry-points/cli/index.test.ts +102 -0
  23. package/src/adapter/entry-points/cli/index.ts +43 -0
  24. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +45 -0
  25. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +7 -0
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +31 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +96 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +20 -2
  29. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +159 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +17 -1
  31. package/src/adapter/entry-points/console/ui-dist/assets/{index-RaFqjB7u.js → index-CYwLv9pf.js} +2 -2
  32. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  33. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  34. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  35. package/src/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.test.ts +4 -1
  36. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +2 -0
  37. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
  38. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  39. package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.test.ts +4 -0
  40. package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +1 -0
  41. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +16 -0
  42. package/src/adapter/repositories/NodeLocalCommandRunner.ts +5 -1
  43. package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +1 -0
  44. package/src/adapter/repositories/NodeTmuxSessionRepository.test.ts +132 -0
  45. package/src/adapter/repositories/NodeTmuxSessionRepository.ts +37 -0
  46. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +38 -0
  47. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -2
  48. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +2 -0
  49. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  50. package/src/domain/usecases/StartPreparationUseCase.test.ts +110 -2
  51. package/src/domain/usecases/StartPreparationUseCase.ts +34 -17
  52. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +1 -0
  53. package/src/domain/usecases/adapter-interfaces/TmuxSessionRepository.ts +4 -0
  54. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +5 -4
  55. package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +9 -11
  56. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +63 -0
  57. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +13 -4
  58. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  59. package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
  60. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  61. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  62. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -0
  63. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
  64. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts +1 -0
  65. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts.map +1 -1
  66. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  67. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  68. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -0
  69. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
  70. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts +1 -0
  71. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts.map +1 -1
  72. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
  73. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +1 -0
  74. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts.map +1 -1
@@ -420,6 +420,51 @@ test('renders the stories tab with non-gray stories, their open item counts, and
420
420
  ).toHaveCount(0);
421
421
  });
422
422
 
423
+ test('shows and hides gray stories with the Show archived toggle button on the stories tab', async ({
424
+ page,
425
+ }) => {
426
+ await page.goto(harness.appRootUrl);
427
+
428
+ await tabByLabel(page, 'Stories').click();
429
+ await expect(activeTabLabel(page)).toHaveText('Stories');
430
+
431
+ await expect(
432
+ page.locator('.console-op-button', { hasText: 'Show archived' }),
433
+ ).toBeVisible();
434
+
435
+ await expect(
436
+ page.locator('.console-story-list-row', {
437
+ hasText: 'regular / workflow improvement',
438
+ }),
439
+ ).toHaveCount(0);
440
+
441
+ await page
442
+ .locator('.console-op-button', { hasText: 'Show archived' })
443
+ .click();
444
+
445
+ await expect(
446
+ page.locator('.console-story-list-row', {
447
+ hasText: 'regular / workflow improvement',
448
+ }),
449
+ ).toBeVisible();
450
+ await expect(
451
+ page.locator('.console-op-button', { hasText: 'Hide archived' }),
452
+ ).toBeVisible();
453
+
454
+ await page
455
+ .locator('.console-op-button', { hasText: 'Hide archived' })
456
+ .click();
457
+
458
+ await expect(
459
+ page.locator('.console-story-list-row', {
460
+ hasText: 'regular / workflow improvement',
461
+ }),
462
+ ).toHaveCount(0);
463
+ await expect(
464
+ page.locator('.console-op-button', { hasText: 'Show archived' }),
465
+ ).toBeVisible();
466
+ });
467
+
423
468
  test('shows the agent label and value in the list row and the agent chip in the detail view', async ({
424
469
  page,
425
470
  }) => {
@@ -295,6 +295,13 @@ const CONSOLE_E2E_STORIES_SNAPSHOT = {
295
295
  storyViewUrl:
296
296
  'https://github.com/orgs/HiromiShikata/projects/6/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
297
297
  },
298
+ {
299
+ storyName: 'regular / workflow improvement',
300
+ storyOptionId: '28415d6c',
301
+ color: 'GRAY',
302
+ openItemCount: 1,
303
+ storyViewUrl: null,
304
+ },
298
305
  {
299
306
  storyName: 'Publish product documentation site',
300
307
  storyOptionId: 'f7cd5cbc',
@@ -47,13 +47,26 @@ const storiesWithUrl = storiesWithoutUrl.map((s) => ({
47
47
  storyViewUrl: `${BASE_VIEW_URL}?sliceBy%5Bvalue%5D=${encodeURI(s.storyName).replace(/#/g, '%23').replace(/&/g, '%26')}`,
48
48
  }));
49
49
 
50
+ const storiesWithArchived = [
51
+ ...storiesWithoutUrl,
52
+ {
53
+ storyName: 'regular / NO STORY; SET STORY FIELD',
54
+ storyOptionId: '28415d6c',
55
+ color: 'GRAY' as const,
56
+ openItemCount: 5,
57
+ storyViewUrl: null,
58
+ },
59
+ ];
60
+
50
61
  const meta: Meta<typeof ConsoleStoryList> = {
51
62
  title: 'Console/ConsoleStoryList',
52
63
  component: ConsoleStoryList,
53
64
  args: {
65
+ showGray: false,
54
66
  onCreateIssue: () => Promise.resolve(),
55
67
  onAddStory: () => Promise.resolve(),
56
68
  onSelectColor: () => undefined,
69
+ onToggleGray: () => undefined,
57
70
  optimisticColors: {},
58
71
  colorChangeInFlight: null,
59
72
  colorErrors: {},
@@ -103,3 +116,21 @@ export const ErrorState: Story = {
103
116
  error: 'HTTP 404',
104
117
  },
105
118
  };
119
+
120
+ export const WithArchivedHidden: Story = {
121
+ args: {
122
+ stories: storiesWithArchived,
123
+ isLoading: false,
124
+ error: null,
125
+ showGray: false,
126
+ },
127
+ };
128
+
129
+ export const WithArchivedShown: Story = {
130
+ args: {
131
+ stories: storiesWithArchived,
132
+ isLoading: false,
133
+ error: null,
134
+ showGray: true,
135
+ },
136
+ };
@@ -19,13 +19,25 @@ const storyEntries: ConsoleStoryEntry[] = [
19
19
  },
20
20
  ];
21
21
 
22
+ const grayStoryEntry: ConsoleStoryEntry = {
23
+ storyName: 'Archived Story',
24
+ storyOptionId: 'gray-id',
25
+ color: 'GRAY',
26
+ openItemCount: 3,
27
+ storyViewUrl: null,
28
+ };
29
+
30
+ const entriesWithGray: ConsoleStoryEntry[] = [...storyEntries, grayStoryEntry];
31
+
22
32
  const defaultProps = {
23
33
  stories: storyEntries,
24
34
  isLoading: false,
25
35
  error: null,
36
+ showGray: false,
26
37
  onCreateIssue: () => Promise.resolve(),
27
38
  onAddStory: () => Promise.resolve(),
28
39
  onSelectColor: () => undefined,
40
+ onToggleGray: () => undefined,
29
41
  optimisticColors: {} as Record<string, ConsoleColor>,
30
42
  colorChangeInFlight: null as string | null,
31
43
  colorErrors: {} as Record<string, string>,
@@ -370,4 +382,88 @@ describe('ConsoleStoryList', () => {
370
382
  });
371
383
  expect(writeText).toHaveBeenCalledWith(storyEntries[0].storyName);
372
384
  });
385
+
386
+ describe('gray story toggle', () => {
387
+ it('hides gray stories when showGray is false', () => {
388
+ const { queryByText } = render(
389
+ <ConsoleStoryList
390
+ {...defaultProps}
391
+ stories={entriesWithGray}
392
+ showGray={false}
393
+ />,
394
+ );
395
+ expect(queryByText('Archived Story')).toBeNull();
396
+ expect(queryByText('TDPM Console port')).toBeInTheDocument();
397
+ });
398
+
399
+ it('shows gray stories when showGray is true', () => {
400
+ const { getByText } = render(
401
+ <ConsoleStoryList
402
+ {...defaultProps}
403
+ stories={entriesWithGray}
404
+ showGray={true}
405
+ />,
406
+ );
407
+ expect(getByText('Archived Story')).toBeInTheDocument();
408
+ });
409
+
410
+ it('shows the toggle button labeled Show archived when at least one gray story exists and showGray is false', () => {
411
+ const { getByRole } = render(
412
+ <ConsoleStoryList
413
+ {...defaultProps}
414
+ stories={entriesWithGray}
415
+ showGray={false}
416
+ />,
417
+ );
418
+ expect(
419
+ getByRole('button', { name: 'Show archived' }),
420
+ ).toBeInTheDocument();
421
+ });
422
+
423
+ it('shows the toggle button labeled Hide archived when showGray is true', () => {
424
+ const { getByRole } = render(
425
+ <ConsoleStoryList
426
+ {...defaultProps}
427
+ stories={entriesWithGray}
428
+ showGray={true}
429
+ />,
430
+ );
431
+ expect(
432
+ getByRole('button', { name: 'Hide archived' }),
433
+ ).toBeInTheDocument();
434
+ });
435
+
436
+ it('does not show the toggle button when no gray stories exist', () => {
437
+ const { queryByRole } = render(
438
+ <ConsoleStoryList {...defaultProps} stories={storyEntries} />,
439
+ );
440
+ expect(queryByRole('button', { name: 'Show archived' })).toBeNull();
441
+ expect(queryByRole('button', { name: 'Hide archived' })).toBeNull();
442
+ });
443
+
444
+ it('calls onToggleGray when the toggle button is clicked', () => {
445
+ const onToggleGray = jest.fn();
446
+ const { getByRole } = render(
447
+ <ConsoleStoryList
448
+ {...defaultProps}
449
+ stories={entriesWithGray}
450
+ showGray={false}
451
+ onToggleGray={onToggleGray}
452
+ />,
453
+ );
454
+ fireEvent.click(getByRole('button', { name: 'Show archived' }));
455
+ expect(onToggleGray).toHaveBeenCalledTimes(1);
456
+ });
457
+
458
+ it('shows No active stories when all stories are gray and showGray is false', () => {
459
+ const { getByText } = render(
460
+ <ConsoleStoryList
461
+ {...defaultProps}
462
+ stories={[grayStoryEntry]}
463
+ showGray={false}
464
+ />,
465
+ );
466
+ expect(getByText('No active stories')).toBeInTheDocument();
467
+ });
468
+ });
373
469
  });
@@ -132,9 +132,11 @@ export type ConsoleStoryListProps = {
132
132
  stories: ConsoleStoryEntry[];
133
133
  isLoading: boolean;
134
134
  error: string | null;
135
+ showGray: boolean;
135
136
  onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
136
137
  onAddStory: (storyName: string) => Promise<void>;
137
138
  onSelectColor: (storyOptionId: string, newColor: ConsoleColor) => void;
139
+ onToggleGray: () => void;
138
140
  optimisticColors: Record<string, ConsoleColor>;
139
141
  colorChangeInFlight: string | null;
140
142
  colorErrors: Record<string, string>;
@@ -144,9 +146,11 @@ export const ConsoleStoryList = ({
144
146
  stories,
145
147
  isLoading,
146
148
  error,
149
+ showGray,
147
150
  onCreateIssue,
148
151
  onAddStory,
149
152
  onSelectColor,
153
+ onToggleGray,
150
154
  optimisticColors,
151
155
  colorChangeInFlight,
152
156
  colorErrors,
@@ -197,13 +201,18 @@ export const ConsoleStoryList = ({
197
201
  onSelectColor(storyOptionId, newColor);
198
202
  };
199
203
 
204
+ const visibleStories = showGray
205
+ ? stories
206
+ : stories.filter((s) => s.color !== 'GRAY');
207
+ const hasGrayStories = stories.some((s) => s.color === 'GRAY');
208
+
200
209
  return (
201
210
  <div className="console-story-list-container">
202
- {stories.length === 0 ? (
211
+ {visibleStories.length === 0 ? (
203
212
  <p className="console-list-empty">No active stories</p>
204
213
  ) : (
205
214
  <ul className="console-story-list">
206
- {stories.map((entry) => {
215
+ {visibleStories.map((entry) => {
207
216
  const displayColor: ConsoleColor =
208
217
  optimisticColors[entry.storyOptionId] ?? entry.color;
209
218
  const palette = colorFromEnum(displayColor);
@@ -285,6 +294,15 @@ export const ConsoleStoryList = ({
285
294
  })}
286
295
  </ul>
287
296
  )}
297
+ {hasGrayStories && (
298
+ <button
299
+ type="button"
300
+ className="console-op-button"
301
+ onClick={onToggleGray}
302
+ >
303
+ {showGray ? 'Hide archived' : 'Show archived'}
304
+ </button>
305
+ )}
288
306
  <div className="console-add-story-section">
289
307
  <button
290
308
  type="button"
@@ -994,6 +994,165 @@ describe('ConsolePage auto-advance tab', () => {
994
994
  });
995
995
  });
996
996
 
997
+ it('shows the stories tab badge count as the number of non-GRAY stories', async () => {
998
+ global.fetch = jest.fn(async (url: string) => {
999
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
1000
+ if (listMatch !== null) {
1001
+ const tab = listMatch[1];
1002
+ if (tab === 'stories') {
1003
+ return {
1004
+ ok: true,
1005
+ status: 200,
1006
+ json: async () => ({
1007
+ generatedAt: '2026-06-19T00:00:00.000Z',
1008
+ stories: [
1009
+ {
1010
+ storyName: 'TDPM Console port',
1011
+ storyOptionId: 'st1',
1012
+ color: 'BLUE',
1013
+ openItemCount: 3,
1014
+ storyViewUrl: null,
1015
+ },
1016
+ {
1017
+ storyName: 'regular / workflow improvement',
1018
+ storyOptionId: 'st2',
1019
+ color: 'GRAY',
1020
+ openItemCount: 2,
1021
+ storyViewUrl: null,
1022
+ },
1023
+ ],
1024
+ defaultNameWithOwner: null,
1025
+ storyOrder: [],
1026
+ }),
1027
+ };
1028
+ }
1029
+ return { ok: true, status: 200, json: async () => listPayload(tab) };
1030
+ }
1031
+ if (url === '/api/projects') {
1032
+ return {
1033
+ ok: true,
1034
+ status: 200,
1035
+ json: async () => ({ pjcodes: ['acme'] }),
1036
+ };
1037
+ }
1038
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
1039
+ }) as unknown as typeof fetch;
1040
+
1041
+ const { container } = render(<ConsolePage />);
1042
+
1043
+ await waitFor(() => {
1044
+ const tabs = [...container.querySelectorAll('.console-tab')];
1045
+ const storiesTab = tabs.find((el) => el.textContent?.includes('Stories'));
1046
+ expect(storiesTab).toBeTruthy();
1047
+ expect(storiesTab?.querySelector('.console-tab-badge')?.textContent).toBe(
1048
+ '1',
1049
+ );
1050
+ });
1051
+ });
1052
+
1053
+ it('reads console-story-show-gray from localStorage and shows gray stories when true', async () => {
1054
+ localStorage.setItem('console-story-show-gray', 'true');
1055
+ window.history.replaceState({}, '', '/projects/acme/stories?k=token');
1056
+ global.fetch = jest.fn(async (url: string) => {
1057
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
1058
+ if (listMatch !== null) {
1059
+ const tab = listMatch[1];
1060
+ if (tab === 'stories') {
1061
+ return {
1062
+ ok: true,
1063
+ status: 200,
1064
+ json: async () => ({
1065
+ generatedAt: '2026-06-19T00:00:00.000Z',
1066
+ stories: [
1067
+ {
1068
+ storyName: 'TDPM Console port',
1069
+ storyOptionId: 'st1',
1070
+ color: 'BLUE',
1071
+ openItemCount: 3,
1072
+ storyViewUrl: null,
1073
+ },
1074
+ {
1075
+ storyName: 'regular / workflow improvement',
1076
+ storyOptionId: 'st2',
1077
+ color: 'GRAY',
1078
+ openItemCount: 2,
1079
+ storyViewUrl: null,
1080
+ },
1081
+ ],
1082
+ defaultNameWithOwner: null,
1083
+ storyOrder: [],
1084
+ }),
1085
+ };
1086
+ }
1087
+ return { ok: true, status: 200, json: async () => listPayload(tab) };
1088
+ }
1089
+ if (url === '/api/projects') {
1090
+ return {
1091
+ ok: true,
1092
+ status: 200,
1093
+ json: async () => ({ pjcodes: ['acme'] }),
1094
+ };
1095
+ }
1096
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
1097
+ }) as unknown as typeof fetch;
1098
+
1099
+ const { findByText } = render(<ConsolePage />);
1100
+ expect(
1101
+ await findByText('regular / workflow improvement'),
1102
+ ).toBeInTheDocument();
1103
+ });
1104
+
1105
+ it('writes true to console-story-show-gray in localStorage when the toggle button is clicked', async () => {
1106
+ window.history.replaceState({}, '', '/projects/acme/stories?k=token');
1107
+ global.fetch = jest.fn(async (url: string) => {
1108
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
1109
+ if (listMatch !== null) {
1110
+ const tab = listMatch[1];
1111
+ if (tab === 'stories') {
1112
+ return {
1113
+ ok: true,
1114
+ status: 200,
1115
+ json: async () => ({
1116
+ generatedAt: '2026-06-19T00:00:00.000Z',
1117
+ stories: [
1118
+ {
1119
+ storyName: 'TDPM Console port',
1120
+ storyOptionId: 'st1',
1121
+ color: 'BLUE',
1122
+ openItemCount: 3,
1123
+ storyViewUrl: null,
1124
+ },
1125
+ {
1126
+ storyName: 'regular / workflow improvement',
1127
+ storyOptionId: 'st2',
1128
+ color: 'GRAY',
1129
+ openItemCount: 2,
1130
+ storyViewUrl: null,
1131
+ },
1132
+ ],
1133
+ defaultNameWithOwner: null,
1134
+ storyOrder: [],
1135
+ }),
1136
+ };
1137
+ }
1138
+ return { ok: true, status: 200, json: async () => listPayload(tab) };
1139
+ }
1140
+ if (url === '/api/projects') {
1141
+ return {
1142
+ ok: true,
1143
+ status: 200,
1144
+ json: async () => ({ pjcodes: ['acme'] }),
1145
+ };
1146
+ }
1147
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
1148
+ }) as unknown as typeof fetch;
1149
+
1150
+ const { findByRole } = render(<ConsolePage />);
1151
+ const toggleBtn = await findByRole('button', { name: 'Show archived' });
1152
+ fireEvent.click(toggleBtn);
1153
+ expect(localStorage.getItem('console-story-show-gray')).toBe('true');
1154
+ });
1155
+
997
1156
  it('navigates to the next project when a completing action fires after the project timer elapses', async () => {
998
1157
  localStorage.setItem(
999
1158
  'tdpm-timer-settings',
@@ -97,7 +97,9 @@ export const ConsolePage = () => {
97
97
  continue;
98
98
  }
99
99
  if (tab.name === 'stories') {
100
- result[tab.name] = snapshot.stories.length;
100
+ result[tab.name] = snapshot.stories.filter(
101
+ (s) => s.color !== 'GRAY',
102
+ ).length;
101
103
  } else {
102
104
  result[tab.name] = countPendingItems(
103
105
  snapshot.items,
@@ -330,6 +332,18 @@ export const ConsolePage = () => {
330
332
 
331
333
  const detailScreenRef = useConsoleSwipeNavigation(handleSwipe);
332
334
 
335
+ const [showGray, setShowGray] = useState(
336
+ () => localStorage.getItem('console-story-show-gray') === 'true',
337
+ );
338
+
339
+ const handleToggleGray = useCallback((): void => {
340
+ setShowGray((prev) => {
341
+ const next = !prev;
342
+ localStorage.setItem('console-story-show-gray', String(next));
343
+ return next;
344
+ });
345
+ }, []);
346
+
333
347
  const storiesSnapshot = snapshots.stories;
334
348
  const storyEntries = storiesSnapshot?.stories ?? [];
335
349
  const defaultNameWithOwner = storiesSnapshot?.defaultNameWithOwner ?? null;
@@ -502,9 +516,11 @@ export const ConsolePage = () => {
502
516
  stories={storyEntries}
503
517
  isLoading={isLoading}
504
518
  error={error}
519
+ showGray={showGray}
505
520
  onCreateIssue={handleCreateIssue}
506
521
  onAddStory={handleStoryAdd}
507
522
  onSelectColor={handleSelectColor}
523
+ onToggleGray={handleToggleGray}
508
524
  optimisticColors={storyOptimisticColors}
509
525
  colorChangeInFlight={storyColorChangeInFlight}
510
526
  colorErrors={storyColorErrors}