github-issue-tower-defence-management 1.90.0 → 1.91.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 (155) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +14 -1
  3. package/bin/adapter/entry-points/cli/index.js +16 -12
  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/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  18. package/jest.config.js +57 -9
  19. package/package.json +17 -13
  20. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  21. package/src/adapter/entry-points/cli/index.ts +30 -12
  22. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  23. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  24. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  25. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  26. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  27. package/src/adapter/entry-points/console/consoleServer.test.ts +5 -4
  28. package/src/adapter/entry-points/console/consoleServer.ts +5 -7
  29. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  30. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  91. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  94. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  97. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +133 -7
  127. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  130. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  131. package/src/adapter/entry-points/console/ui/tsconfig.json +1 -0
  132. package/src/adapter/entry-points/console/ui/vite.config.ts +5 -0
  133. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  134. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  135. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  136. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  137. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  138. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  139. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  140. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  141. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  142. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  143. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  144. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  145. package/types/adapter/entry-points/console/consoleServer.d.ts +2 -3
  146. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  147. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  148. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  149. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  150. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  151. package/bin/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  152. package/bin/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
  153. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -78
  154. package/src/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  155. package/src/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
@@ -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
+ };
@@ -0,0 +1,79 @@
1
+ import { fireEvent, render, waitFor } from '@testing-library/react';
2
+ import {
3
+ consoleListItemsFixture,
4
+ consoleStatusOptionsFixture,
5
+ consoleStoryColorsFixture,
6
+ consoleStoryOptionsFixture,
7
+ } from '../fixtures';
8
+ import type { ConsoleCaches } from '../hooks/useConsoleCaches';
9
+ import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
10
+ import { ResourceCache } from '../lib/resourceCache';
11
+ import { ConsoleItemDetailContainer } from './ConsoleItemDetailContainer';
12
+
13
+ jest.mock('../lib/mermaidLoader', () => ({
14
+ renderMermaidToSvg: jest.fn(async () => '<svg></svg>'),
15
+ }));
16
+
17
+ const prItem = consoleListItemsFixture[0];
18
+
19
+ const buildCaches = (): ConsoleCaches => {
20
+ const client = {
21
+ fetchItemBody: async () => '# body',
22
+ fetchComments: async () => [],
23
+ fetchPrFiles: async () => [],
24
+ fetchPrCommits: async () => [],
25
+ fetchRelatedPrs: async () => [],
26
+ fetchIssueState: async () => ({
27
+ state: 'open',
28
+ merged: false,
29
+ isPullRequest: true,
30
+ }),
31
+ };
32
+ return {
33
+ client,
34
+ body: new ResourceCache(client.fetchItemBody),
35
+ comments: new ResourceCache(client.fetchComments),
36
+ files: new ResourceCache(client.fetchPrFiles),
37
+ commits: new ResourceCache(client.fetchPrCommits),
38
+ relatedPrs: new ResourceCache(client.fetchRelatedPrs),
39
+ state: new ResourceCache(client.fetchIssueState),
40
+ };
41
+ };
42
+
43
+ const buildOperations = (): ConsoleOperationsApi => ({
44
+ reviewPullRequest: jest.fn(async () => {}),
45
+ setNextActionDate: jest.fn(async () => {}),
46
+ setStory: jest.fn(async () => {}),
47
+ setStatus: jest.fn(async () => {}),
48
+ setInTmuxByHuman: jest.fn(async () => {}),
49
+ closeIssue: jest.fn(async () => {}),
50
+ });
51
+
52
+ describe('ConsoleItemDetailContainer', () => {
53
+ it('wires the review action to the operations api for a PR item', async () => {
54
+ const operations = buildOperations();
55
+ const { getByText } = render(
56
+ <ConsoleItemDetailContainer
57
+ tab="prs"
58
+ item={prItem}
59
+ caches={buildCaches()}
60
+ operations={operations}
61
+ statusOptions={consoleStatusOptionsFixture}
62
+ storyOptions={consoleStoryOptionsFixture}
63
+ storyColors={consoleStoryColorsFixture}
64
+ storyName="TDPM Console port"
65
+ overlayStatus={null}
66
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
67
+ />,
68
+ );
69
+ await waitFor(() => {
70
+ expect(getByText('Approve')).toBeInTheDocument();
71
+ });
72
+ fireEvent.click(getByText('Approve'));
73
+ expect(operations.reviewPullRequest).toHaveBeenCalledWith(
74
+ prItem,
75
+ prItem.url,
76
+ 'approve',
77
+ );
78
+ });
79
+ });
@@ -0,0 +1,109 @@
1
+ import { ConsoleItemDetail } from '../components/ConsoleItemDetail';
2
+ import { ConsoleOperationBar } from '../components/ConsoleOperationBar';
3
+ import { resolveStoryColorEnum } from '../grouping';
4
+ import type { ConsoleCaches } from '../hooks/useConsoleCaches';
5
+ import { useConsoleItemDetailData } from '../hooks/useConsoleItemDetailData';
6
+ import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
7
+ import type { ConsoleOperationHandlers } from '../operations';
8
+ import type {
9
+ ConsoleColor,
10
+ ConsoleFieldOption,
11
+ ConsoleListItem,
12
+ ConsoleOverlayStatus,
13
+ ConsoleStoryColorSource,
14
+ ConsoleTabName,
15
+ } from '../types';
16
+
17
+ export type ConsoleItemDetailContainerProps = {
18
+ tab: ConsoleTabName;
19
+ item: ConsoleListItem;
20
+ caches: ConsoleCaches;
21
+ operations: ConsoleOperationsApi;
22
+ statusOptions: ConsoleFieldOption[];
23
+ storyOptions: ConsoleFieldOption[];
24
+ storyColors: ConsoleStoryColorSource;
25
+ storyName: string | null;
26
+ overlayStatus: ConsoleOverlayStatus | null;
27
+ now: number;
28
+ };
29
+
30
+ export const ConsoleItemDetailContainer = ({
31
+ tab,
32
+ item,
33
+ caches,
34
+ operations,
35
+ statusOptions,
36
+ storyOptions,
37
+ storyColors,
38
+ storyName,
39
+ overlayStatus,
40
+ now,
41
+ }: ConsoleItemDetailContainerProps) => {
42
+ const detail = useConsoleItemDetailData(caches, item);
43
+ const hasPullRequest = item.isPr || detail.relatedPullRequests.length > 0;
44
+
45
+ const handlers: ConsoleOperationHandlers = {
46
+ onReview: (action) => {
47
+ const prUrl = item.isPr
48
+ ? item.url
49
+ : (detail.relatedPullRequests[0]?.pullRequest.url ?? item.url);
50
+ void operations.reviewPullRequest(item, prUrl, action);
51
+ },
52
+ onSetNextActionDate: (action) => {
53
+ void operations.setNextActionDate(item, action);
54
+ },
55
+ onSetStory: (option: ConsoleFieldOption) => {
56
+ void operations.setStory(item, option);
57
+ },
58
+ onSetStatus: (option: ConsoleFieldOption) => {
59
+ void operations.setStatus(item, option);
60
+ },
61
+ onSetInTmuxByHuman: (option: ConsoleFieldOption) => {
62
+ void operations.setInTmuxByHuman(item, option);
63
+ },
64
+ onClose: (action) => {
65
+ void operations.closeIssue(item, action);
66
+ },
67
+ };
68
+
69
+ const resolvedStoryName =
70
+ storyName ?? (item.story.trim() !== '' ? item.story : null);
71
+ const storyColorEnum: ConsoleColor | null =
72
+ resolvedStoryName !== null
73
+ ? resolveStoryColorEnum(storyColors, resolvedStoryName)
74
+ : null;
75
+
76
+ return (
77
+ <ConsoleItemDetail
78
+ item={item}
79
+ storyName={resolvedStoryName}
80
+ storyColorEnum={storyColorEnum}
81
+ overlayStatus={overlayStatus}
82
+ state={detail.state}
83
+ body={detail.body}
84
+ bodyIsLoading={detail.bodyIsLoading}
85
+ bodyError={detail.bodyError}
86
+ comments={detail.comments}
87
+ commentsAreLoading={detail.commentsAreLoading}
88
+ commentsError={detail.commentsError}
89
+ files={detail.files}
90
+ filesAreLoading={detail.filesAreLoading}
91
+ filesError={detail.filesError}
92
+ commits={detail.commits}
93
+ commitsAreLoading={detail.commitsAreLoading}
94
+ commitsError={detail.commitsError}
95
+ relatedPullRequests={detail.relatedPullRequests}
96
+ now={now}
97
+ operationBar={
98
+ <ConsoleOperationBar
99
+ tab={tab}
100
+ item={item}
101
+ hasPullRequest={hasPullRequest}
102
+ statusOptions={statusOptions}
103
+ storyOptions={storyOptions}
104
+ handlers={handlers}
105
+ />
106
+ }
107
+ />
108
+ );
109
+ };
@@ -0,0 +1,74 @@
1
+ import { fireEvent, render, waitFor } from '@testing-library/react';
2
+ import { ConsolePage } from './ConsolePage';
3
+
4
+ jest.mock('../lib/mermaidLoader', () => ({
5
+ renderMermaidToSvg: jest.fn(async () => '<svg></svg>'),
6
+ }));
7
+
8
+ const listPayload = (tab: string) => ({
9
+ pjcode: 'umino',
10
+ generatedAt: '2026-06-19T00:00:00.000Z',
11
+ statusOptions: [{ id: 's1', name: 'Awaiting Workspace', color: 'BLUE' }],
12
+ storyOptions: [{ id: 'st1', name: 'TDPM Console port', color: 'BLUE' }],
13
+ storyColors: { 'TDPM Console port': { color: 'BLUE' } },
14
+ items:
15
+ tab === 'prs'
16
+ ? [
17
+ {
18
+ number: 851,
19
+ title: 'Add serveConsole subcommand',
20
+ url: 'https://github.com/o/r/pull/851',
21
+ repo: 'o/r',
22
+ nameWithOwner: 'o/r',
23
+ projectItemId: 'PVTI_1',
24
+ itemId: 'PVTI_1',
25
+ isPr: true,
26
+ story: 'TDPM Console port',
27
+ labels: [],
28
+ createdAt: '2026-06-17T00:00:00.000Z',
29
+ },
30
+ ]
31
+ : [],
32
+ });
33
+
34
+ const installFetch = (): void => {
35
+ const fetchMock = jest.fn(async (url: string) => {
36
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
37
+ if (listMatch !== null) {
38
+ return {
39
+ ok: true,
40
+ status: 200,
41
+ json: async () => listPayload(listMatch[1]),
42
+ };
43
+ }
44
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
45
+ });
46
+ global.fetch = fetchMock as unknown as typeof fetch;
47
+ };
48
+
49
+ describe('ConsolePage', () => {
50
+ beforeEach(() => {
51
+ localStorage.clear();
52
+ window.history.replaceState({}, '', '/projects/umino/prs?k=token');
53
+ installFetch();
54
+ });
55
+
56
+ it('renders the tab bar with the active tab and the story-grouped list', async () => {
57
+ const { getByText } = render(<ConsolePage />);
58
+ await waitFor(() => {
59
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
60
+ });
61
+ expect(getByText('Awaiting Quality Check')).toBeInTheDocument();
62
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
63
+ });
64
+
65
+ it('opens the detail view when an item is selected', async () => {
66
+ const { getByText, findByText } = render(<ConsolePage />);
67
+ await waitFor(() => {
68
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
69
+ });
70
+ fireEvent.click(getByText('Add serveConsole subcommand'));
71
+ expect(await findByText('← Back to list')).toBeInTheDocument();
72
+ expect(getByText('Approve')).toBeInTheDocument();
73
+ });
74
+ });