github-issue-tower-defence-management 1.89.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 (160) hide show
  1. package/.github/CODEOWNERS +1 -2
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/adapter/entry-points/cli/index.js +16 -12
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  11. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  12. package/bin/adapter/entry-points/console/consoleServer.js +43 -17
  13. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  15. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  16. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/jest.config.js +57 -9
  20. package/package.json +17 -13
  21. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  22. package/src/adapter/entry-points/cli/index.ts +30 -12
  23. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  25. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  26. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  27. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  28. package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
  29. package/src/adapter/entry-points/console/consoleServer.ts +53 -23
  30. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  94. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  97. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
  132. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  135. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  136. package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
  137. package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
  138. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  140. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  141. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  143. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  144. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  145. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  146. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  147. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  148. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  149. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  150. package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
  151. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  152. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  153. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  154. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  155. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  156. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  157. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
  158. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
  159. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  160. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
@@ -0,0 +1,243 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ type ConsoleIntmuxRequest,
4
+ type ConsoleReviewRequest,
5
+ type ConsoleTriageRequest,
6
+ postConsoleOperation,
7
+ } from '../lib/consoleApi';
8
+ import {
9
+ type ConsoleCloseAction,
10
+ type ConsoleNextActionDateAction,
11
+ type ConsoleReviewAction,
12
+ TOTALLY_WRONG_COMMENT_BODY,
13
+ UNNECESSARY_COMMENT_BODY,
14
+ } from '../operations';
15
+ import { overlayKeyForItem } from '../overlay';
16
+ import type {
17
+ ConsoleFieldOption,
18
+ ConsoleListItem,
19
+ ConsoleTabName,
20
+ } from '../types';
21
+ import type { ConsoleOverlayState } from './useConsoleOverlay';
22
+ import { useConsoleToken } from './useConsoleToken';
23
+
24
+ export const REVIEW_OPERATION_PATH = '/api/review';
25
+ export const TRIAGE_OPERATION_PATH = '/api/triage';
26
+ export const INTMUX_OPERATION_PATH = '/api/intmux';
27
+
28
+ export type ConsoleOperationsApi = {
29
+ reviewPullRequest: (
30
+ item: ConsoleListItem,
31
+ prUrl: string,
32
+ action: ConsoleReviewAction,
33
+ ) => Promise<void>;
34
+ setNextActionDate: (
35
+ item: ConsoleListItem,
36
+ action: ConsoleNextActionDateAction,
37
+ ) => Promise<void>;
38
+ setStory: (
39
+ item: ConsoleListItem,
40
+ option: ConsoleFieldOption,
41
+ ) => Promise<void>;
42
+ setStatus: (
43
+ item: ConsoleListItem,
44
+ option: ConsoleFieldOption,
45
+ ) => Promise<void>;
46
+ setInTmuxByHuman: (
47
+ item: ConsoleListItem,
48
+ option: ConsoleFieldOption,
49
+ ) => Promise<void>;
50
+ closeIssue: (
51
+ item: ConsoleListItem,
52
+ action: ConsoleCloseAction,
53
+ ) => Promise<void>;
54
+ };
55
+
56
+ const reviewRequest = (
57
+ pjcode: string,
58
+ item: ConsoleListItem,
59
+ prUrl: string,
60
+ action: ConsoleReviewAction,
61
+ ): ConsoleReviewRequest => {
62
+ if (action === 'approve') {
63
+ return {
64
+ pjcode,
65
+ action: 'approve',
66
+ prUrl,
67
+ projectItemId: item.projectItemId,
68
+ };
69
+ }
70
+ if (action === 'request_changes') {
71
+ return {
72
+ pjcode,
73
+ action: 'request_changes',
74
+ prUrl,
75
+ projectItemId: item.projectItemId,
76
+ commentBody: '',
77
+ };
78
+ }
79
+ if (action === 'totally_wrong') {
80
+ return {
81
+ pjcode,
82
+ action: 'close',
83
+ prUrl,
84
+ projectItemId: item.projectItemId,
85
+ commentBody: TOTALLY_WRONG_COMMENT_BODY,
86
+ };
87
+ }
88
+ return {
89
+ pjcode,
90
+ action: 'close',
91
+ prUrl,
92
+ projectItemId: item.projectItemId,
93
+ commentBody: UNNECESSARY_COMMENT_BODY,
94
+ };
95
+ };
96
+
97
+ const missingPjcodeError = (): Error =>
98
+ new Error('No project specified in the URL path.');
99
+
100
+ export const useConsoleOperations = (
101
+ pjcode: string | null,
102
+ mode: ConsoleTabName,
103
+ overlayState: ConsoleOverlayState,
104
+ ): ConsoleOperationsApi => {
105
+ const { appendToken } = useConsoleToken();
106
+ const { patchOverlay } = overlayState;
107
+
108
+ const markDone = useCallback(
109
+ (item: ConsoleListItem) => {
110
+ patchOverlay(overlayKeyForItem(item), { done: true }, mode);
111
+ },
112
+ [patchOverlay, mode],
113
+ );
114
+
115
+ const reviewPullRequest = useCallback(
116
+ async (
117
+ item: ConsoleListItem,
118
+ prUrl: string,
119
+ action: ConsoleReviewAction,
120
+ ) => {
121
+ if (pjcode === null) {
122
+ throw missingPjcodeError();
123
+ }
124
+ await postConsoleOperation(
125
+ appendToken,
126
+ REVIEW_OPERATION_PATH,
127
+ reviewRequest(pjcode, item, prUrl, action),
128
+ );
129
+ markDone(item);
130
+ },
131
+ [pjcode, appendToken, markDone],
132
+ );
133
+
134
+ const setNextActionDate = useCallback(
135
+ async (item: ConsoleListItem, action: ConsoleNextActionDateAction) => {
136
+ if (pjcode === null) {
137
+ throw missingPjcodeError();
138
+ }
139
+ const request: ConsoleTriageRequest = {
140
+ pjcode,
141
+ action,
142
+ issueUrl: item.url,
143
+ projectItemId: item.projectItemId,
144
+ };
145
+ await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
146
+ if (mode === 'todo-by-human') {
147
+ markDone(item);
148
+ }
149
+ },
150
+ [pjcode, appendToken, markDone, mode],
151
+ );
152
+
153
+ const setStory = useCallback(
154
+ async (item: ConsoleListItem, option: ConsoleFieldOption) => {
155
+ if (pjcode === null) {
156
+ throw missingPjcodeError();
157
+ }
158
+ const request: ConsoleTriageRequest = {
159
+ pjcode,
160
+ action: 'set_story',
161
+ issueUrl: item.url,
162
+ projectItemId: item.projectItemId,
163
+ storyOptionId: option.id,
164
+ };
165
+ await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
166
+ patchOverlay(
167
+ overlayKeyForItem(item),
168
+ { done: true, story: { name: option.name, color: option.color } },
169
+ mode,
170
+ );
171
+ },
172
+ [pjcode, appendToken, patchOverlay, mode],
173
+ );
174
+
175
+ const setStatus = useCallback(
176
+ async (item: ConsoleListItem, option: ConsoleFieldOption) => {
177
+ if (pjcode === null) {
178
+ throw missingPjcodeError();
179
+ }
180
+ const request: ConsoleTriageRequest = {
181
+ pjcode,
182
+ action: 'set_status',
183
+ issueUrl: item.url,
184
+ projectItemId: item.projectItemId,
185
+ statusName: option.name,
186
+ };
187
+ await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
188
+ patchOverlay(
189
+ overlayKeyForItem(item),
190
+ { done: true, status: { name: option.name, color: option.color } },
191
+ mode,
192
+ );
193
+ },
194
+ [pjcode, appendToken, patchOverlay, mode],
195
+ );
196
+
197
+ const setInTmuxByHuman = useCallback(
198
+ async (item: ConsoleListItem, option: ConsoleFieldOption) => {
199
+ if (pjcode === null) {
200
+ throw missingPjcodeError();
201
+ }
202
+ const request: ConsoleIntmuxRequest = {
203
+ pjcode,
204
+ action: 'set_intmux',
205
+ issueUrl: item.url,
206
+ projectItemId: item.projectItemId,
207
+ };
208
+ await postConsoleOperation(appendToken, INTMUX_OPERATION_PATH, request);
209
+ patchOverlay(
210
+ overlayKeyForItem(item),
211
+ { done: true, status: { name: option.name, color: option.color } },
212
+ mode,
213
+ );
214
+ },
215
+ [pjcode, appendToken, patchOverlay, mode],
216
+ );
217
+
218
+ const closeIssue = useCallback(
219
+ async (item: ConsoleListItem, action: ConsoleCloseAction) => {
220
+ if (pjcode === null) {
221
+ throw missingPjcodeError();
222
+ }
223
+ const request: ConsoleTriageRequest = {
224
+ pjcode,
225
+ action,
226
+ issueUrl: item.url,
227
+ projectItemId: item.projectItemId,
228
+ };
229
+ await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
230
+ markDone(item);
231
+ },
232
+ [pjcode, appendToken, markDone],
233
+ );
234
+
235
+ return {
236
+ reviewPullRequest,
237
+ setNextActionDate,
238
+ setStory,
239
+ setStatus,
240
+ setInTmuxByHuman,
241
+ closeIssue,
242
+ };
243
+ };
@@ -0,0 +1,40 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { overlayStorageKey } from '../overlay';
3
+ import { useConsoleOverlay } from './useConsoleOverlay';
4
+
5
+ describe('useConsoleOverlay', () => {
6
+ beforeEach(() => {
7
+ localStorage.clear();
8
+ });
9
+
10
+ it('starts from the persisted overlay', () => {
11
+ localStorage.setItem(
12
+ overlayStorageKey('umino'),
13
+ JSON.stringify({ PVTI_1: { ts: 5, mode: 'prs', done: true } }),
14
+ );
15
+ const { result } = renderHook(() => useConsoleOverlay('umino'));
16
+ expect(result.current.overlay.PVTI_1?.done).toBe(true);
17
+ });
18
+
19
+ it('patches and persists overlay entries with timestamp and mode', () => {
20
+ const { result } = renderHook(() => useConsoleOverlay('umino'));
21
+ act(() => {
22
+ result.current.patchOverlay('PVTI_2', { done: true }, 'triage');
23
+ });
24
+ expect(result.current.overlay.PVTI_2?.done).toBe(true);
25
+ expect(result.current.overlay.PVTI_2?.mode).toBe('triage');
26
+ const stored = JSON.parse(
27
+ localStorage.getItem(overlayStorageKey('umino')) ?? '{}',
28
+ );
29
+ expect(stored.PVTI_2.done).toBe(true);
30
+ });
31
+
32
+ it('ignores malformed persisted entries', () => {
33
+ localStorage.setItem(
34
+ overlayStorageKey('umino'),
35
+ JSON.stringify({ bad: { no: 'ts' } }),
36
+ );
37
+ const { result } = renderHook(() => useConsoleOverlay('umino'));
38
+ expect(result.current.overlay.bad).toBeUndefined();
39
+ });
40
+ });
@@ -0,0 +1,71 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { overlayStorageKey, writeOverlayEntry } from '../overlay';
3
+ import type {
4
+ ConsoleOverlay,
5
+ ConsoleOverlayEntry,
6
+ ConsoleTabName,
7
+ } from '../types';
8
+
9
+ const isOverlayEntry = (value: unknown): value is ConsoleOverlayEntry =>
10
+ value !== null &&
11
+ typeof value === 'object' &&
12
+ typeof (value as { ts?: unknown }).ts === 'number';
13
+
14
+ const readOverlay = (pjcode: string): ConsoleOverlay => {
15
+ if (typeof localStorage === 'undefined') {
16
+ return {};
17
+ }
18
+ const raw = localStorage.getItem(overlayStorageKey(pjcode));
19
+ if (raw === null) {
20
+ return {};
21
+ }
22
+ const parsed: unknown = JSON.parse(raw);
23
+ if (parsed === null || typeof parsed !== 'object') {
24
+ return {};
25
+ }
26
+ const overlay: ConsoleOverlay = {};
27
+ for (const [key, value] of Object.entries(parsed)) {
28
+ if (isOverlayEntry(value)) {
29
+ overlay[key] = value;
30
+ }
31
+ }
32
+ return overlay;
33
+ };
34
+
35
+ const persistOverlay = (pjcode: string, overlay: ConsoleOverlay): void => {
36
+ if (typeof localStorage !== 'undefined') {
37
+ localStorage.setItem(overlayStorageKey(pjcode), JSON.stringify(overlay));
38
+ }
39
+ };
40
+
41
+ export type ConsoleOverlayState = {
42
+ overlay: ConsoleOverlay;
43
+ patchOverlay: (
44
+ key: string,
45
+ patch: Partial<Omit<ConsoleOverlayEntry, 'ts' | 'mode'>>,
46
+ mode: ConsoleTabName,
47
+ ) => void;
48
+ };
49
+
50
+ export const useConsoleOverlay = (pjcode: string): ConsoleOverlayState => {
51
+ const [overlay, setOverlay] = useState<ConsoleOverlay>(() =>
52
+ readOverlay(pjcode),
53
+ );
54
+
55
+ const patchOverlay = useCallback(
56
+ (
57
+ key: string,
58
+ patch: Partial<Omit<ConsoleOverlayEntry, 'ts' | 'mode'>>,
59
+ mode: ConsoleTabName,
60
+ ) => {
61
+ setOverlay((current) => {
62
+ const next = writeOverlayEntry(current, key, patch, mode, Date.now());
63
+ persistOverlay(pjcode, next);
64
+ return next;
65
+ });
66
+ },
67
+ [pjcode],
68
+ );
69
+
70
+ return { overlay, patchOverlay };
71
+ };
@@ -0,0 +1,24 @@
1
+ import { parsePjcodeFromPath } from './useConsolePjcode';
2
+
3
+ describe('parsePjcodeFromPath', () => {
4
+ it('extracts the pjcode from a projects path', () => {
5
+ expect(parsePjcodeFromPath('/projects/umino')).toBe('umino');
6
+ expect(parsePjcodeFromPath('/projects/umino/prs')).toBe('umino');
7
+ expect(parsePjcodeFromPath('/projects/xmile/triage')).toBe('xmile');
8
+ });
9
+
10
+ it('tolerates a trailing slash', () => {
11
+ expect(parsePjcodeFromPath('/projects/utage3/')).toBe('utage3');
12
+ });
13
+
14
+ it('returns null when the path is not under projects', () => {
15
+ expect(parsePjcodeFromPath('/')).toBeNull();
16
+ expect(parsePjcodeFromPath('/index.html')).toBeNull();
17
+ expect(parsePjcodeFromPath('/assets/app.js')).toBeNull();
18
+ });
19
+
20
+ it('returns null when no pjcode segment follows projects', () => {
21
+ expect(parsePjcodeFromPath('/projects')).toBeNull();
22
+ expect(parsePjcodeFromPath('/projects/')).toBeNull();
23
+ });
24
+ });
@@ -0,0 +1,17 @@
1
+ export const parsePjcodeFromPath = (pathname: string): string | null => {
2
+ const segments = pathname.split('/').filter((segment) => segment.length > 0);
3
+ if (segments.length < 2 || segments[0] !== 'projects') {
4
+ return null;
5
+ }
6
+ const pjcode = segments[1];
7
+ if (pjcode.length === 0) {
8
+ return null;
9
+ }
10
+ return pjcode;
11
+ };
12
+
13
+ export const useConsolePjcode = (): string | null => {
14
+ const pathname =
15
+ typeof window === 'undefined' ? '' : window.location.pathname;
16
+ return parsePjcodeFromPath(pathname);
17
+ };
@@ -0,0 +1,41 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { ResourceCache } from '../lib/resourceCache';
3
+ import { useConsoleResource } from './useConsoleResource';
4
+
5
+ describe('useConsoleResource', () => {
6
+ it('loads the resource and exposes loading then data', async () => {
7
+ const cache = new ResourceCache<string>(async () => 'loaded');
8
+ const { result } = renderHook(() =>
9
+ useConsoleResource(cache, 'k', 'u', 'fallback'),
10
+ );
11
+ expect(result.current.isLoading).toBe(true);
12
+ expect(result.current.data).toBe('fallback');
13
+ await waitFor(() => {
14
+ expect(result.current.isLoading).toBe(false);
15
+ });
16
+ expect(result.current.data).toBe('loaded');
17
+ expect(result.current.error).toBeNull();
18
+ });
19
+
20
+ it('stays idle with the fallback when the key is null', () => {
21
+ const cache = new ResourceCache<string>(async () => 'loaded');
22
+ const { result } = renderHook(() =>
23
+ useConsoleResource(cache, null, null, 'fallback'),
24
+ );
25
+ expect(result.current.isLoading).toBe(false);
26
+ expect(result.current.data).toBe('fallback');
27
+ });
28
+
29
+ it('surfaces a fetch error', async () => {
30
+ const cache = new ResourceCache<string>(async () => {
31
+ throw new Error('boom');
32
+ });
33
+ const { result } = renderHook(() =>
34
+ useConsoleResource(cache, 'k', 'u', 'fallback'),
35
+ );
36
+ await waitFor(() => {
37
+ expect(result.current.error).toBe('boom');
38
+ });
39
+ expect(result.current.isLoading).toBe(false);
40
+ });
41
+ });
@@ -0,0 +1,57 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { ResourceCache } from '../lib/resourceCache';
3
+
4
+ export type ConsoleResourceState<T> = {
5
+ data: T;
6
+ isLoading: boolean;
7
+ error: string | null;
8
+ };
9
+
10
+ export const useConsoleResource = <T>(
11
+ cache: ResourceCache<T>,
12
+ key: string | null,
13
+ url: string | null,
14
+ fallback: T,
15
+ ): ConsoleResourceState<T> => {
16
+ const cached = key !== null ? cache.peek(key) : undefined;
17
+ const [data, setData] = useState<T>(cached ?? fallback);
18
+ const [isLoading, setIsLoading] = useState<boolean>(
19
+ key !== null && cached === undefined,
20
+ );
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ useEffect(() => {
24
+ if (key === null || url === null) {
25
+ return;
26
+ }
27
+ const existing = cache.peek(key);
28
+ if (existing !== undefined) {
29
+ setData(existing);
30
+ setIsLoading(false);
31
+ setError(null);
32
+ return;
33
+ }
34
+ let cancelled = false;
35
+ setIsLoading(true);
36
+ setError(null);
37
+ cache
38
+ .load(key, url)
39
+ .then((value) => {
40
+ if (!cancelled) {
41
+ setData(value);
42
+ setIsLoading(false);
43
+ }
44
+ })
45
+ .catch((cause: unknown) => {
46
+ if (!cancelled) {
47
+ setError(cause instanceof Error ? cause.message : String(cause));
48
+ setIsLoading(false);
49
+ }
50
+ });
51
+ return () => {
52
+ cancelled = true;
53
+ };
54
+ }, [cache, key, url]);
55
+
56
+ return { data, isLoading, error };
57
+ };
@@ -0,0 +1,63 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { useConsoleTabData } from './useConsoleTabData';
3
+
4
+ describe('useConsoleTabData', () => {
5
+ beforeEach(() => {
6
+ localStorage.clear();
7
+ window.history.replaceState({}, '', '/?k=token');
8
+ });
9
+
10
+ it('fetches every tab once at startup and parses snapshots', async () => {
11
+ const fetchMock = jest.fn(async (url: string) => ({
12
+ ok: true,
13
+ status: 200,
14
+ json: async () => ({
15
+ pjcode: 'umino',
16
+ generatedAt: '2026-06-19T00:00:00.000Z',
17
+ statusOptions: [{ id: 's1', name: 'Unread', color: 'ORANGE' }],
18
+ storyColors: {},
19
+ items: url.includes('/prs/')
20
+ ? [{ number: 1, itemId: 'PVTI_1', projectItemId: 'PVTI_1' }]
21
+ : [],
22
+ }),
23
+ }));
24
+ global.fetch = fetchMock as unknown as typeof fetch;
25
+
26
+ const { result } = renderHook(() => useConsoleTabData('umino'));
27
+ await waitFor(() => {
28
+ expect(result.current.isLoading).toBe(false);
29
+ });
30
+ expect(fetchMock).toHaveBeenCalledTimes(5);
31
+ expect(fetchMock).toHaveBeenCalledWith(
32
+ expect.stringContaining('/projects/umino/prs/list.json'),
33
+ );
34
+ expect(result.current.snapshots.prs?.items.length).toBe(1);
35
+ expect(result.current.snapshots.prs?.generatedAt).toBe(
36
+ '2026-06-19T00:00:00.000Z',
37
+ );
38
+ });
39
+
40
+ it('surfaces an error when a tab fetch fails', async () => {
41
+ const fetchMock = jest.fn(async () => ({
42
+ ok: false,
43
+ status: 500,
44
+ json: async () => ({}),
45
+ }));
46
+ global.fetch = fetchMock as unknown as typeof fetch;
47
+ const { result } = renderHook(() => useConsoleTabData('umino'));
48
+ await waitFor(() => {
49
+ expect(result.current.error).toBe('HTTP 500');
50
+ });
51
+ });
52
+
53
+ it('reports an error and fetches nothing when no pjcode is in the URL', async () => {
54
+ const fetchMock = jest.fn();
55
+ global.fetch = fetchMock as unknown as typeof fetch;
56
+ const { result } = renderHook(() => useConsoleTabData(null));
57
+ await waitFor(() => {
58
+ expect(result.current.isLoading).toBe(false);
59
+ });
60
+ expect(result.current.error).toBe('No project specified in the URL path.');
61
+ expect(fetchMock).not.toHaveBeenCalled();
62
+ });
63
+ });
@@ -0,0 +1,129 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type {
3
+ ConsoleFieldOption,
4
+ ConsoleListItem,
5
+ ConsoleStoryColorSource,
6
+ ConsoleTabName,
7
+ } from '../types';
8
+ import { CONSOLE_TABS } from '../types';
9
+ import { useConsoleToken } from './useConsoleToken';
10
+
11
+ export type ConsoleTabSnapshot = {
12
+ items: ConsoleListItem[];
13
+ generatedAt: string;
14
+ statusOptions: ConsoleFieldOption[];
15
+ storyOptions: ConsoleFieldOption[];
16
+ storyColors: ConsoleStoryColorSource;
17
+ };
18
+
19
+ export type ConsoleTabDataState = {
20
+ snapshots: Record<ConsoleTabName, ConsoleTabSnapshot | null>;
21
+ isLoading: boolean;
22
+ error: string | null;
23
+ };
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ value !== null && typeof value === 'object' && !Array.isArray(value);
27
+
28
+ const parseItems = (payload: unknown): ConsoleListItem[] => {
29
+ if (!isRecord(payload) || !Array.isArray(payload.items)) {
30
+ return [];
31
+ }
32
+ return payload.items.filter(isRecord) as unknown as ConsoleListItem[];
33
+ };
34
+
35
+ const parseOptions = (value: unknown): ConsoleFieldOption[] => {
36
+ if (!Array.isArray(value)) {
37
+ return [];
38
+ }
39
+ return value.filter(isRecord) as unknown as ConsoleFieldOption[];
40
+ };
41
+
42
+ const parseSnapshot = (payload: unknown): ConsoleTabSnapshot => ({
43
+ items: parseItems(payload),
44
+ generatedAt:
45
+ isRecord(payload) && typeof payload.generatedAt === 'string'
46
+ ? payload.generatedAt
47
+ : '',
48
+ statusOptions: isRecord(payload) ? parseOptions(payload.statusOptions) : [],
49
+ storyOptions: isRecord(payload) ? parseOptions(payload.storyOptions) : [],
50
+ storyColors:
51
+ isRecord(payload) && isRecord(payload.storyColors)
52
+ ? (payload.storyColors as ConsoleStoryColorSource)
53
+ : {},
54
+ });
55
+
56
+ const emptySnapshots = (): Record<
57
+ ConsoleTabName,
58
+ ConsoleTabSnapshot | null
59
+ > => {
60
+ const result = {} as Record<ConsoleTabName, ConsoleTabSnapshot | null>;
61
+ for (const tab of CONSOLE_TABS) {
62
+ result[tab.name] = null;
63
+ }
64
+ return result;
65
+ };
66
+
67
+ const buildListUrl = (pjcode: string, tab: ConsoleTabName): string =>
68
+ `/projects/${pjcode}/${tab}/list.json`;
69
+
70
+ export const useConsoleTabData = (
71
+ pjcode: string | null,
72
+ ): ConsoleTabDataState => {
73
+ const { appendToken } = useConsoleToken();
74
+ const [snapshots, setSnapshots] =
75
+ useState<Record<ConsoleTabName, ConsoleTabSnapshot | null>>(emptySnapshots);
76
+ const [isLoading, setIsLoading] = useState<boolean>(true);
77
+ const [error, setError] = useState<string | null>(null);
78
+
79
+ useEffect(() => {
80
+ let cancelled = false;
81
+ setIsLoading(true);
82
+ setError(null);
83
+
84
+ if (pjcode === null) {
85
+ setSnapshots(emptySnapshots());
86
+ setIsLoading(false);
87
+ setError('No project specified in the URL path.');
88
+ return () => {
89
+ cancelled = true;
90
+ };
91
+ }
92
+
93
+ Promise.all(
94
+ CONSOLE_TABS.map(async (tab) => {
95
+ const url = appendToken(buildListUrl(pjcode, tab.name));
96
+ const response = await fetch(url);
97
+ if (!response.ok) {
98
+ throw new Error(`HTTP ${response.status}`);
99
+ }
100
+ const payload: unknown = await response.json();
101
+ return [tab.name, parseSnapshot(payload)] as const;
102
+ }),
103
+ )
104
+ .then((entries) => {
105
+ if (cancelled) {
106
+ return;
107
+ }
108
+ const next = emptySnapshots();
109
+ for (const [name, snapshot] of entries) {
110
+ next[name] = snapshot;
111
+ }
112
+ setSnapshots(next);
113
+ setIsLoading(false);
114
+ })
115
+ .catch((cause: unknown) => {
116
+ if (cancelled) {
117
+ return;
118
+ }
119
+ setError(cause instanceof Error ? cause.message : String(cause));
120
+ setIsLoading(false);
121
+ });
122
+
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [pjcode, appendToken]);
127
+
128
+ return { snapshots, isLoading, error };
129
+ };