github-issue-tower-defence-management 1.90.0 → 1.91.1

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 (174) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +19 -5
  3. package/bin/adapter/entry-points/cli/index.js +17 -13
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  9. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  11. package/bin/adapter/entry-points/console/consoleServer.js +3 -4
  12. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  13. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  15. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  16. package/bin/adapter/entry-points/handlers/consoleListsWriter.js +1 -0
  17. package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  19. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  20. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +3 -0
  21. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
  22. package/jest.config.js +57 -9
  23. package/package.json +17 -13
  24. package/src/adapter/entry-points/cli/index.test.ts +18 -3
  25. package/src/adapter/entry-points/cli/index.ts +32 -14
  26. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  27. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  28. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  29. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  30. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  31. package/src/adapter/entry-points/console/consoleServer.test.ts +5 -4
  32. package/src/adapter/entry-points/console/consoleServer.ts +5 -7
  33. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  34. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +27 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +36 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +50 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.stories.tsx +22 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.test.tsx +38 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.tsx +65 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.stories.tsx +28 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.test.tsx +42 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.tsx +55 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.stories.tsx +29 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.test.tsx +55 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +66 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.stories.tsx +25 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.test.tsx +53 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.tsx +53 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +79 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +81 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +229 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.stories.tsx +82 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.test.tsx +52 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.tsx +32 -0
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.stories.tsx +31 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +40 -0
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +88 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +26 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +32 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +36 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/{ConsoleProjectHeader.stories.tsx → layout/ConsoleProjectSummary.stories.tsx} +5 -5
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleProjectSummary.test.tsx +14 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/{ConsoleProjectHeader.tsx → layout/ConsoleProjectSummary.tsx} +3 -1
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +70 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +59 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +41 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +60 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +87 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +68 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +25 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +34 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.stories.tsx +27 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.test.tsx +24 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.tsx +28 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.stories.tsx +14 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.test.tsx +21 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.tsx +26 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.stories.tsx +20 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.test.tsx +42 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +28 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.stories.tsx +55 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.test.tsx +85 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.tsx +58 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.stories.tsx +14 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.test.tsx +33 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +34 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.stories.tsx +17 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.test.tsx +49 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.tsx +66 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.stories.tsx +17 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.test.tsx +39 -0
  94. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.tsx +42 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  97. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/logic/colors.test.ts +34 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/logic/colors.ts +73 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/logic/fileStatus.test.ts +35 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/logic/fileStatus.ts +21 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.test.ts +91 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.ts +79 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/logic/itemIcons.test.ts +97 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/logic/itemIcons.ts +95 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/logic/relativeTime.test.ts +52 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/logic/relativeTime.ts +51 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +141 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  132. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -11
  135. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +244 -0
  136. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  137. package/src/adapter/entry-points/console/ui/tsconfig.json +1 -0
  138. package/src/adapter/entry-points/console/ui/vite.config.ts +5 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  140. package/src/adapter/entry-points/console/ui-dist/assets/index-PtVrAcBb.js +100 -0
  141. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  142. package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +27 -2
  143. package/src/adapter/entry-points/handlers/consoleListsWriter.ts +1 -0
  144. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  145. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  146. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +26 -0
  147. package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +17 -1
  148. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  149. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  150. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  151. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  152. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  153. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  154. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  155. package/types/adapter/entry-points/console/consoleServer.d.ts +2 -3
  156. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  157. package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
  158. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  159. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  160. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  161. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  162. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +2 -1
  163. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
  164. package/bin/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  165. package/bin/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
  166. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +0 -44
  167. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +0 -58
  168. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +0 -34
  169. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +0 -32
  170. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +0 -47
  171. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -78
  172. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +0 -69
  173. package/src/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  174. package/src/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
@@ -0,0 +1,21 @@
1
+ export type ConsoleFileStatusBadge = {
2
+ label: string;
3
+ color: string;
4
+ };
5
+
6
+ export const fileStatusBadge = (status: string): ConsoleFileStatusBadge => {
7
+ switch (status) {
8
+ case 'added':
9
+ return { label: 'A', color: '#3fb950' };
10
+ case 'modified':
11
+ return { label: 'M', color: '#d29922' };
12
+ case 'removed':
13
+ return { label: 'D', color: '#f85149' };
14
+ case 'renamed':
15
+ return { label: 'R', color: '#a371f7' };
16
+ case 'changed':
17
+ return { label: 'M', color: '#d29922' };
18
+ default:
19
+ return { label: '?', color: '#8b949e' };
20
+ }
21
+ };
@@ -0,0 +1,91 @@
1
+ import {
2
+ buildConsoleListRows,
3
+ CONSOLE_NO_STORY_LABEL,
4
+ resolveItemStory,
5
+ resolveStoryColorEnum,
6
+ } from './grouping';
7
+ import type { ConsoleListItem, ConsoleOverlay } from './types';
8
+
9
+ const item = (
10
+ overrides: Partial<ConsoleListItem> &
11
+ Pick<ConsoleListItem, 'number' | 'story'>,
12
+ ): ConsoleListItem => ({
13
+ title: `Item ${overrides.number}`,
14
+ url: `https://github.com/o/r/issues/${overrides.number}`,
15
+ repo: 'o/r',
16
+ nameWithOwner: 'o/r',
17
+ projectItemId: `PVTI_${overrides.number}`,
18
+ itemId: `PVTI_${overrides.number}`,
19
+ isPr: false,
20
+ labels: [],
21
+ createdAt: '2026-06-10T00:00:00.000Z',
22
+ ...overrides,
23
+ });
24
+
25
+ describe('resolveStoryColorEnum', () => {
26
+ it('reads a wrapped color object shape', () => {
27
+ expect(resolveStoryColorEnum({ s: { color: 'BLUE' } }, 's')).toBe('BLUE');
28
+ });
29
+
30
+ it('reads a bare enum shape', () => {
31
+ expect(resolveStoryColorEnum({ s: 'RED' }, 's')).toBe('RED');
32
+ });
33
+
34
+ it('returns null for an unknown story', () => {
35
+ expect(resolveStoryColorEnum({}, 's')).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe('resolveItemStory', () => {
40
+ it('prefers the overlay story name', () => {
41
+ const overlay: ConsoleOverlay = {
42
+ PVTI_1: {
43
+ ts: 1,
44
+ mode: 'triage',
45
+ story: { name: 'Overlay', color: 'BLUE' },
46
+ },
47
+ };
48
+ expect(
49
+ resolveItemStory(item({ number: 1, story: 'Original' }), overlay),
50
+ ).toBe('Overlay');
51
+ });
52
+
53
+ it('uses the trimmed item story when no overlay story exists', () => {
54
+ expect(resolveItemStory(item({ number: 2, story: ' Real ' }), {})).toBe(
55
+ ' Real ',
56
+ );
57
+ });
58
+
59
+ it('falls back to the no-story label when empty', () => {
60
+ expect(resolveItemStory(item({ number: 3, story: ' ' }), {})).toBe(
61
+ CONSOLE_NO_STORY_LABEL,
62
+ );
63
+ });
64
+ });
65
+
66
+ describe('buildConsoleListRows', () => {
67
+ it('inserts a group header whenever the story changes and keeps array order', () => {
68
+ const items = [
69
+ item({ number: 1, story: 'Alpha' }),
70
+ item({ number: 2, story: 'Alpha' }),
71
+ item({ number: 3, story: 'Beta' }),
72
+ item({ number: 4, story: 'Alpha' }),
73
+ ];
74
+ const rows = buildConsoleListRows(items, {});
75
+ expect(rows.map((row) => row.kind)).toEqual([
76
+ 'group-header',
77
+ 'item',
78
+ 'item',
79
+ 'group-header',
80
+ 'item',
81
+ 'group-header',
82
+ 'item',
83
+ ]);
84
+ const firstHeader = rows[0];
85
+ expect(firstHeader.kind === 'group-header' && firstHeader.count).toBe(3);
86
+ });
87
+
88
+ it('returns no rows for an empty list', () => {
89
+ expect(buildConsoleListRows([], {})).toEqual([]);
90
+ });
91
+ });
@@ -0,0 +1,79 @@
1
+ import type {
2
+ ConsoleColor,
3
+ ConsoleListItem,
4
+ ConsoleOverlay,
5
+ ConsoleStoryColorSource,
6
+ } from './types';
7
+
8
+ export const CONSOLE_NO_STORY_LABEL = '(No story)';
9
+
10
+ export const resolveStoryColorEnum = (
11
+ storyColors: ConsoleStoryColorSource,
12
+ storyName: string,
13
+ ): ConsoleColor | null => {
14
+ const entry = storyColors[storyName];
15
+ if (entry === undefined) {
16
+ return null;
17
+ }
18
+ if (typeof entry === 'string') {
19
+ return entry;
20
+ }
21
+ return entry.color;
22
+ };
23
+
24
+ export const resolveItemStory = (
25
+ item: ConsoleListItem,
26
+ overlay: ConsoleOverlay,
27
+ ): string => {
28
+ const overlayKey =
29
+ item.projectItemId !== '' ? item.projectItemId : item.itemId;
30
+ const overlayEntry = overlay[overlayKey];
31
+ if (
32
+ overlayEntry?.story?.name !== undefined &&
33
+ overlayEntry.story.name !== ''
34
+ ) {
35
+ return overlayEntry.story.name;
36
+ }
37
+ const trimmed = item.story.trim();
38
+ return trimmed !== '' ? item.story : CONSOLE_NO_STORY_LABEL;
39
+ };
40
+
41
+ export type ConsoleListGroupRow = {
42
+ kind: 'group-header';
43
+ story: string;
44
+ count: number;
45
+ };
46
+
47
+ export type ConsoleItemSummary = {
48
+ kind: 'item';
49
+ item: ConsoleListItem;
50
+ };
51
+
52
+ export type ConsoleListRow = ConsoleListGroupRow | ConsoleItemSummary;
53
+
54
+ export const buildConsoleListRows = (
55
+ items: ConsoleListItem[],
56
+ overlay: ConsoleOverlay,
57
+ ): ConsoleListRow[] => {
58
+ const storyCounts = new Map<string, number>();
59
+ for (const item of items) {
60
+ const story = resolveItemStory(item, overlay);
61
+ storyCounts.set(story, (storyCounts.get(story) ?? 0) + 1);
62
+ }
63
+
64
+ const rows: ConsoleListRow[] = [];
65
+ let previousStory: string | null = null;
66
+ for (const item of items) {
67
+ const story = resolveItemStory(item, overlay);
68
+ if (story !== previousStory) {
69
+ rows.push({
70
+ kind: 'group-header',
71
+ story,
72
+ count: storyCounts.get(story) ?? 0,
73
+ });
74
+ previousStory = story;
75
+ }
76
+ rows.push({ kind: 'item', item });
77
+ }
78
+ return rows;
79
+ };
@@ -0,0 +1,97 @@
1
+ import { CONSOLE_ITEM_ICONS, resolveConsoleItemIconKind } from './itemIcons';
2
+
3
+ describe('resolveConsoleItemIconKind', () => {
4
+ it('resolves a draft pull request to prDraft', () => {
5
+ expect(
6
+ resolveConsoleItemIconKind({
7
+ isPr: true,
8
+ state: 'open',
9
+ merged: false,
10
+ isDraft: true,
11
+ stateReason: '',
12
+ }),
13
+ ).toBe('prDraft');
14
+ });
15
+
16
+ it('resolves a merged pull request to prMerged', () => {
17
+ expect(
18
+ resolveConsoleItemIconKind({
19
+ isPr: true,
20
+ state: 'closed',
21
+ merged: true,
22
+ isDraft: false,
23
+ stateReason: '',
24
+ }),
25
+ ).toBe('prMerged');
26
+ });
27
+
28
+ it('resolves a closed pull request to prClosed', () => {
29
+ expect(
30
+ resolveConsoleItemIconKind({
31
+ isPr: true,
32
+ state: 'closed',
33
+ merged: false,
34
+ isDraft: false,
35
+ stateReason: '',
36
+ }),
37
+ ).toBe('prClosed');
38
+ });
39
+
40
+ it('resolves an open pull request to prOpen', () => {
41
+ expect(
42
+ resolveConsoleItemIconKind({
43
+ isPr: true,
44
+ state: 'open',
45
+ merged: false,
46
+ isDraft: false,
47
+ stateReason: '',
48
+ }),
49
+ ).toBe('prOpen');
50
+ });
51
+
52
+ it('resolves a completed closed issue to issueClosed', () => {
53
+ expect(
54
+ resolveConsoleItemIconKind({
55
+ isPr: false,
56
+ state: 'closed',
57
+ merged: false,
58
+ isDraft: false,
59
+ stateReason: 'completed',
60
+ }),
61
+ ).toBe('issueClosed');
62
+ });
63
+
64
+ it('resolves a not-planned closed issue to issueClosedNotPlanned', () => {
65
+ expect(
66
+ resolveConsoleItemIconKind({
67
+ isPr: false,
68
+ state: 'closed',
69
+ merged: false,
70
+ isDraft: false,
71
+ stateReason: 'not_planned',
72
+ }),
73
+ ).toBe('issueClosedNotPlanned');
74
+ });
75
+
76
+ it('resolves an open issue to issueOpen', () => {
77
+ expect(
78
+ resolveConsoleItemIconKind({
79
+ isPr: false,
80
+ state: 'open',
81
+ merged: false,
82
+ isDraft: false,
83
+ stateReason: '',
84
+ }),
85
+ ).toBe('issueOpen');
86
+ });
87
+
88
+ it('uses the documented colors for each kind', () => {
89
+ expect(CONSOLE_ITEM_ICONS.prDraft.color).toBe('#8b949e');
90
+ expect(CONSOLE_ITEM_ICONS.prMerged.color).toBe('#a371f7');
91
+ expect(CONSOLE_ITEM_ICONS.prClosed.color).toBe('#f85149');
92
+ expect(CONSOLE_ITEM_ICONS.prOpen.color).toBe('#3fb950');
93
+ expect(CONSOLE_ITEM_ICONS.issueClosedNotPlanned.color).toBe('#848d97');
94
+ expect(CONSOLE_ITEM_ICONS.issueClosed.color).toBe('#a371f7');
95
+ expect(CONSOLE_ITEM_ICONS.issueOpen.color).toBe('#3fb950');
96
+ });
97
+ });
@@ -0,0 +1,95 @@
1
+ export type ConsoleItemIconKind =
2
+ | 'issueOpen'
3
+ | 'issueClosed'
4
+ | 'issueClosedNotPlanned'
5
+ | 'prOpen'
6
+ | 'prMerged'
7
+ | 'prClosed'
8
+ | 'prDraft';
9
+
10
+ export type ConsoleItemIconDefinition = {
11
+ color: string;
12
+ paths: string[];
13
+ };
14
+
15
+ export const CONSOLE_ITEM_ICONS: Record<
16
+ ConsoleItemIconKind,
17
+ ConsoleItemIconDefinition
18
+ > = {
19
+ issueOpen: {
20
+ color: '#3fb950',
21
+ paths: [
22
+ 'M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
23
+ 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z',
24
+ ],
25
+ },
26
+ issueClosed: {
27
+ color: '#a371f7',
28
+ paths: [
29
+ 'M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z',
30
+ 'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z',
31
+ ],
32
+ },
33
+ issueClosedNotPlanned: {
34
+ color: '#848d97',
35
+ paths: [
36
+ 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z',
37
+ ],
38
+ },
39
+ prOpen: {
40
+ color: '#3fb950',
41
+ paths: [
42
+ 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z',
43
+ ],
44
+ },
45
+ prMerged: {
46
+ color: '#a371f7',
47
+ paths: [
48
+ 'M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z',
49
+ ],
50
+ },
51
+ prClosed: {
52
+ color: '#f85149',
53
+ paths: [
54
+ 'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z',
55
+ ],
56
+ },
57
+ prDraft: {
58
+ color: '#8b949e',
59
+ paths: [
60
+ 'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z',
61
+ ],
62
+ },
63
+ };
64
+
65
+ export type ConsoleItemIconInput = {
66
+ isPr: boolean;
67
+ state: string;
68
+ merged: boolean;
69
+ isDraft: boolean;
70
+ stateReason: string;
71
+ };
72
+
73
+ export const resolveConsoleItemIconKind = ({
74
+ isPr,
75
+ state,
76
+ merged,
77
+ isDraft,
78
+ stateReason,
79
+ }: ConsoleItemIconInput): ConsoleItemIconKind => {
80
+ if (isPr) {
81
+ if (isDraft) {
82
+ return 'prDraft';
83
+ }
84
+ if (merged) {
85
+ return 'prMerged';
86
+ }
87
+ return state === 'closed' ? 'prClosed' : 'prOpen';
88
+ }
89
+ if (state === 'closed') {
90
+ return stateReason === 'not_planned'
91
+ ? 'issueClosedNotPlanned'
92
+ : 'issueClosed';
93
+ }
94
+ return 'issueOpen';
95
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ IN_TMUX_BY_HUMAN_NAME,
3
+ isTodoByHumanTab,
4
+ STATUS_BUTTON_NAMES,
5
+ TOTALLY_WRONG_COMMENT_BODY,
6
+ UNNECESSARY_COMMENT_BODY,
7
+ } from './operations';
8
+
9
+ describe('operation constants', () => {
10
+ it('defines the totally wrong comment body', () => {
11
+ expect(TOTALLY_WRONG_COMMENT_BODY).toBe('totally wrong');
12
+ });
13
+
14
+ it('defines the unnecessary comment body', () => {
15
+ expect(UNNECESSARY_COMMENT_BODY).toBe('This pull request is unnecessary.');
16
+ });
17
+
18
+ it('lists the status buttons left to right', () => {
19
+ expect(STATUS_BUTTON_NAMES).toEqual([
20
+ 'In Tmux by agent',
21
+ 'In Tmux by human',
22
+ 'Todo by human',
23
+ 'Awaiting Workspace',
24
+ ]);
25
+ });
26
+
27
+ it('names the in-tmux-by-human status', () => {
28
+ expect(IN_TMUX_BY_HUMAN_NAME).toBe('In Tmux by human');
29
+ });
30
+ });
31
+
32
+ describe('isTodoByHumanTab', () => {
33
+ it('is true only for the todo-by-human tab', () => {
34
+ expect(isTodoByHumanTab('todo-by-human')).toBe(true);
35
+ expect(isTodoByHumanTab('prs')).toBe(false);
36
+ });
37
+ });
@@ -0,0 +1,35 @@
1
+ import type { ConsoleFieldOption, ConsoleTabName } from './types';
2
+
3
+ export const TOTALLY_WRONG_COMMENT_BODY = 'totally wrong';
4
+ export const UNNECESSARY_COMMENT_BODY = 'This pull request is unnecessary.';
5
+
6
+ export type ConsoleReviewAction =
7
+ | 'approve'
8
+ | 'request_changes'
9
+ | 'unnecessary'
10
+ | 'totally_wrong';
11
+
12
+ export type ConsoleNextActionDateAction = 'snooze_1day' | 'snooze_1week';
13
+
14
+ export type ConsoleCloseAction = 'close' | 'close_not_planned';
15
+
16
+ export type ConsoleOperationHandlers = {
17
+ onReview: (action: ConsoleReviewAction) => void;
18
+ onSetNextActionDate: (action: ConsoleNextActionDateAction) => void;
19
+ onSetStory: (option: ConsoleFieldOption) => void;
20
+ onSetStatus: (option: ConsoleFieldOption) => void;
21
+ onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
22
+ onClose: (action: ConsoleCloseAction) => void;
23
+ };
24
+
25
+ export const STATUS_BUTTON_NAMES: string[] = [
26
+ 'In Tmux by agent',
27
+ 'In Tmux by human',
28
+ 'Todo by human',
29
+ 'Awaiting Workspace',
30
+ ];
31
+
32
+ export const IN_TMUX_BY_HUMAN_NAME = 'In Tmux by human';
33
+
34
+ export const isTodoByHumanTab = (tab: ConsoleTabName): boolean =>
35
+ tab === 'todo-by-human';
@@ -0,0 +1,124 @@
1
+ import {
2
+ countPendingItems,
3
+ filterPendingItems,
4
+ getOverlayEntry,
5
+ isOverlayEntryActedForMode,
6
+ isOverlayEntryExpiredForMode,
7
+ overlayKeyForItem,
8
+ overlayStorageKey,
9
+ parseGeneratedAtMs,
10
+ writeOverlayEntry,
11
+ } from './overlay';
12
+ import type { ConsoleListItem, ConsoleOverlay } from './types';
13
+
14
+ const item = (number: number): ConsoleListItem => ({
15
+ number,
16
+ title: `Item ${number}`,
17
+ url: `https://github.com/o/r/issues/${number}`,
18
+ repo: 'o/r',
19
+ nameWithOwner: 'o/r',
20
+ projectItemId: `PVTI_${number}`,
21
+ itemId: `PVTI_${number}`,
22
+ isPr: false,
23
+ story: 'Story',
24
+ labels: [],
25
+ createdAt: '2026-06-10T00:00:00.000Z',
26
+ });
27
+
28
+ describe('overlay helpers', () => {
29
+ it('builds the per-project storage key', () => {
30
+ expect(overlayStorageKey('umino')).toBe('pv_overlay_umino');
31
+ });
32
+
33
+ it('uses the projectItemId as the overlay key when present', () => {
34
+ expect(overlayKeyForItem(item(5))).toBe('PVTI_5');
35
+ });
36
+
37
+ it('parses a generatedAt timestamp', () => {
38
+ expect(parseGeneratedAtMs('2026-06-10T00:00:00.000Z')).toBe(
39
+ Date.parse('2026-06-10T00:00:00.000Z'),
40
+ );
41
+ expect(parseGeneratedAtMs('not-a-date')).toBe(0);
42
+ });
43
+ });
44
+
45
+ describe('mode-aware expiry', () => {
46
+ it('expires a same-mode entry written before the snapshot', () => {
47
+ expect(
48
+ isOverlayEntryExpiredForMode(
49
+ { ts: 100, mode: 'prs', done: true },
50
+ 200,
51
+ 'prs',
52
+ ),
53
+ ).toBe(true);
54
+ });
55
+
56
+ it('does not expire an entry written in a different mode', () => {
57
+ expect(
58
+ isOverlayEntryExpiredForMode(
59
+ { ts: 100, mode: 'triage', done: true },
60
+ 200,
61
+ 'prs',
62
+ ),
63
+ ).toBe(false);
64
+ });
65
+
66
+ it('does not expire an entry written after the snapshot', () => {
67
+ expect(
68
+ isOverlayEntryExpiredForMode(
69
+ { ts: 300, mode: 'prs', done: true },
70
+ 200,
71
+ 'prs',
72
+ ),
73
+ ).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('counts driven to zero do not revive on tab switch', () => {
78
+ it('keeps a done item subtracted in its own mode', () => {
79
+ const overlay: ConsoleOverlay = {
80
+ PVTI_1: { ts: 500, mode: 'prs', done: true },
81
+ };
82
+ const generatedAtMs = 400;
83
+ expect(countPendingItems([item(1)], overlay, generatedAtMs, 'prs')).toBe(0);
84
+ });
85
+
86
+ it('treats a done entry from another mode as still acted', () => {
87
+ const overlay: ConsoleOverlay = {
88
+ PVTI_1: { ts: 100, mode: 'triage', done: true },
89
+ };
90
+ expect(isOverlayEntryActedForMode(overlay.PVTI_1, 999, 'prs')).toBe(true);
91
+ expect(countPendingItems([item(1)], overlay, 999, 'prs')).toBe(0);
92
+ });
93
+
94
+ it('revives the count only when a newer same-mode snapshot supersedes the entry', () => {
95
+ const overlay: ConsoleOverlay = {
96
+ PVTI_1: { ts: 100, mode: 'prs', done: true },
97
+ };
98
+ expect(countPendingItems([item(1)], overlay, 200, 'prs')).toBe(1);
99
+ });
100
+ });
101
+
102
+ describe('filterPendingItems', () => {
103
+ it('drops acted items and keeps the rest', () => {
104
+ const overlay: ConsoleOverlay = {
105
+ PVTI_1: { ts: 500, mode: 'prs', done: true },
106
+ };
107
+ const result = filterPendingItems([item(1), item(2)], overlay, 400, 'prs');
108
+ expect(result.map((entry) => entry.number)).toEqual([2]);
109
+ });
110
+ });
111
+
112
+ describe('getOverlayEntry and writeOverlayEntry', () => {
113
+ it('returns null for an expired entry', () => {
114
+ const overlay: ConsoleOverlay = {
115
+ PVTI_1: { ts: 100, mode: 'prs', done: true },
116
+ };
117
+ expect(getOverlayEntry(overlay, item(1), 200, 'prs')).toBeNull();
118
+ });
119
+
120
+ it('stamps the timestamp and mode on write', () => {
121
+ const next = writeOverlayEntry({}, 'PVTI_1', { done: true }, 'prs', 1234);
122
+ expect(next.PVTI_1).toEqual({ done: true, ts: 1234, mode: 'prs' });
123
+ });
124
+ });
@@ -0,0 +1,101 @@
1
+ import type {
2
+ ConsoleListItem,
3
+ ConsoleOverlay,
4
+ ConsoleOverlayEntry,
5
+ ConsoleTabName,
6
+ } from './types';
7
+
8
+ export const overlayStorageKey = (pjcode: string): string =>
9
+ `pv_overlay_${pjcode}`;
10
+
11
+ export const overlayKeyForItem = (item: ConsoleListItem): string =>
12
+ item.projectItemId !== '' ? item.projectItemId : item.itemId;
13
+
14
+ export const parseGeneratedAtMs = (generatedAt: string): number => {
15
+ const parsed = Date.parse(generatedAt);
16
+ return Number.isNaN(parsed) ? 0 : parsed;
17
+ };
18
+
19
+ export const isOverlayEntryExpiredForMode = (
20
+ entry: ConsoleOverlayEntry,
21
+ generatedAtMs: number,
22
+ mode: ConsoleTabName,
23
+ ): boolean =>
24
+ entry.ts > 0 &&
25
+ generatedAtMs > 0 &&
26
+ entry.mode === mode &&
27
+ entry.ts < generatedAtMs;
28
+
29
+ export const getOverlayEntry = (
30
+ overlay: ConsoleOverlay,
31
+ item: ConsoleListItem,
32
+ generatedAtMs: number,
33
+ mode: ConsoleTabName,
34
+ ): ConsoleOverlayEntry | null => {
35
+ const entry = overlay[overlayKeyForItem(item)];
36
+ if (entry === undefined) {
37
+ return null;
38
+ }
39
+ if (isOverlayEntryExpiredForMode(entry, generatedAtMs, mode)) {
40
+ return null;
41
+ }
42
+ return entry;
43
+ };
44
+
45
+ export const isOverlayEntryActedForMode = (
46
+ entry: ConsoleOverlayEntry | undefined,
47
+ generatedAtMs: number,
48
+ mode: ConsoleTabName,
49
+ ): boolean => {
50
+ if (entry === undefined || entry.done !== true) {
51
+ return false;
52
+ }
53
+ return !isOverlayEntryExpiredForMode(entry, generatedAtMs, mode);
54
+ };
55
+
56
+ export const countPendingItems = (
57
+ items: ConsoleListItem[],
58
+ overlay: ConsoleOverlay,
59
+ generatedAtMs: number,
60
+ mode: ConsoleTabName,
61
+ ): number =>
62
+ items.filter(
63
+ (item) =>
64
+ !isOverlayEntryActedForMode(
65
+ overlay[overlayKeyForItem(item)],
66
+ generatedAtMs,
67
+ mode,
68
+ ),
69
+ ).length;
70
+
71
+ export const filterPendingItems = (
72
+ items: ConsoleListItem[],
73
+ overlay: ConsoleOverlay,
74
+ generatedAtMs: number,
75
+ mode: ConsoleTabName,
76
+ ): ConsoleListItem[] =>
77
+ items.filter(
78
+ (item) =>
79
+ !isOverlayEntryActedForMode(
80
+ overlay[overlayKeyForItem(item)],
81
+ generatedAtMs,
82
+ mode,
83
+ ),
84
+ );
85
+
86
+ export const writeOverlayEntry = (
87
+ overlay: ConsoleOverlay,
88
+ key: string,
89
+ patch: Partial<Omit<ConsoleOverlayEntry, 'ts' | 'mode'>>,
90
+ mode: ConsoleTabName,
91
+ now: number,
92
+ ): ConsoleOverlay => {
93
+ const existing = overlay[key];
94
+ const next: ConsoleOverlayEntry = {
95
+ ...(existing ?? {}),
96
+ ...patch,
97
+ ts: now,
98
+ mode,
99
+ };
100
+ return { ...overlay, [key]: next };
101
+ };