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,187 @@
1
+ import type {
2
+ ConsoleChangedFile,
3
+ ConsoleComment,
4
+ ConsoleCommit,
5
+ ConsoleIssueState,
6
+ ConsoleRelatedPullRequest,
7
+ } from '../logic/types';
8
+
9
+ export type ConsoleApiClient = {
10
+ fetchItemBody: (url: string) => Promise<string>;
11
+ fetchComments: (url: string) => Promise<ConsoleComment[]>;
12
+ fetchPrFiles: (url: string) => Promise<ConsoleChangedFile[]>;
13
+ fetchPrCommits: (url: string) => Promise<ConsoleCommit[]>;
14
+ fetchRelatedPrs: (url: string) => Promise<ConsoleRelatedPullRequest[]>;
15
+ fetchIssueState: (url: string) => Promise<ConsoleIssueState>;
16
+ };
17
+
18
+ export type ConsoleReviewRequest = {
19
+ pjcode: string;
20
+ action: string;
21
+ prUrl: string;
22
+ projectItemId: string;
23
+ commentBody?: string;
24
+ changedFilePath?: string;
25
+ };
26
+
27
+ export type ConsoleTriageRequest = {
28
+ pjcode: string;
29
+ action: string;
30
+ issueUrl: string;
31
+ projectItemId: string;
32
+ statusName?: string;
33
+ storyOptionId?: string;
34
+ commentBody?: string;
35
+ };
36
+
37
+ export type ConsoleIntmuxRequest = {
38
+ pjcode: string;
39
+ action: 'set_intmux';
40
+ issueUrl: string;
41
+ projectItemId: string;
42
+ };
43
+
44
+ type AppendToken = (url: string) => string;
45
+
46
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
47
+ value !== null && typeof value === 'object' && !Array.isArray(value);
48
+
49
+ const getString = (value: unknown): string =>
50
+ typeof value === 'string' ? value : '';
51
+
52
+ const getNumber = (value: unknown): number =>
53
+ typeof value === 'number' ? value : 0;
54
+
55
+ const getBoolean = (value: unknown): boolean => value === true;
56
+
57
+ const requestJson = async (
58
+ appendToken: AppendToken,
59
+ apiPath: string,
60
+ resourceUrl: string,
61
+ ): Promise<unknown> => {
62
+ const target = appendToken(
63
+ `${apiPath}?url=${encodeURIComponent(resourceUrl)}`,
64
+ );
65
+ const response = await fetch(target);
66
+ if (!response.ok) {
67
+ throw new Error(`HTTP ${response.status}`);
68
+ }
69
+ return response.json();
70
+ };
71
+
72
+ const parseComments = (payload: unknown): ConsoleComment[] => {
73
+ if (!isRecord(payload) || !Array.isArray(payload.comments)) {
74
+ return [];
75
+ }
76
+ return payload.comments.filter(isRecord).map((comment) => ({
77
+ author: getString(comment.author),
78
+ body: getString(comment.body),
79
+ createdAt: getString(comment.createdAt),
80
+ }));
81
+ };
82
+
83
+ const parseFiles = (payload: unknown): ConsoleChangedFile[] => {
84
+ if (!isRecord(payload) || !Array.isArray(payload.files)) {
85
+ return [];
86
+ }
87
+ return payload.files.filter(isRecord).map((file) => ({
88
+ path: getString(file.path) || getString(file.filename),
89
+ additions: getNumber(file.additions),
90
+ deletions: getNumber(file.deletions),
91
+ status: getString(file.status),
92
+ patch: typeof file.patch === 'string' ? file.patch : null,
93
+ }));
94
+ };
95
+
96
+ const parseCommits = (payload: unknown): ConsoleCommit[] => {
97
+ if (!isRecord(payload) || !Array.isArray(payload.commits)) {
98
+ return [];
99
+ }
100
+ return payload.commits.filter(isRecord).map((commit) => ({
101
+ sha: getString(commit.sha),
102
+ message: getString(commit.message),
103
+ author: getString(commit.author),
104
+ authoredAt: getString(commit.authoredAt),
105
+ }));
106
+ };
107
+
108
+ const parseSummary = (value: unknown): ConsoleRelatedPullRequest['summary'] => {
109
+ if (!isRecord(value)) {
110
+ return null;
111
+ }
112
+ return {
113
+ title: getString(value.title),
114
+ body: getString(value.body),
115
+ additions: getNumber(value.additions),
116
+ deletions: getNumber(value.deletions),
117
+ changedFiles: getNumber(value.changedFiles),
118
+ };
119
+ };
120
+
121
+ const parseRelatedPrs = (payload: unknown): ConsoleRelatedPullRequest[] => {
122
+ if (!isRecord(payload) || !Array.isArray(payload.relatedPullRequests)) {
123
+ return [];
124
+ }
125
+ return payload.relatedPullRequests.filter(isRecord).map((pr) => ({
126
+ url: getString(pr.url),
127
+ branchName: typeof pr.branchName === 'string' ? pr.branchName : null,
128
+ createdAt: getString(pr.createdAt),
129
+ isDraft: getBoolean(pr.isDraft),
130
+ isConflicted: getBoolean(pr.isConflicted),
131
+ isPassedAllCiJob: getBoolean(pr.isPassedAllCiJob),
132
+ isCiStateSuccess: getBoolean(pr.isCiStateSuccess),
133
+ isResolvedAllReviewComments: getBoolean(pr.isResolvedAllReviewComments),
134
+ isBranchOutOfDate: getBoolean(pr.isBranchOutOfDate),
135
+ missingRequiredCheckNames: Array.isArray(pr.missingRequiredCheckNames)
136
+ ? pr.missingRequiredCheckNames.filter(
137
+ (name): name is string => typeof name === 'string',
138
+ )
139
+ : [],
140
+ summary: parseSummary(pr.summary),
141
+ }));
142
+ };
143
+
144
+ const parseState = (payload: unknown): ConsoleIssueState => {
145
+ if (!isRecord(payload)) {
146
+ return { state: 'open', merged: false, isPullRequest: false };
147
+ }
148
+ return {
149
+ state: getString(payload.state) || 'open',
150
+ merged: getBoolean(payload.merged),
151
+ isPullRequest: getBoolean(payload.isPullRequest),
152
+ };
153
+ };
154
+
155
+ export const createConsoleApiClient = (
156
+ appendToken: AppendToken,
157
+ ): ConsoleApiClient => ({
158
+ fetchItemBody: async (url) => {
159
+ const payload = await requestJson(appendToken, './api/itembody', url);
160
+ return isRecord(payload) ? getString(payload.body) : '';
161
+ },
162
+ fetchComments: async (url) =>
163
+ parseComments(await requestJson(appendToken, './api/comments', url)),
164
+ fetchPrFiles: async (url) =>
165
+ parseFiles(await requestJson(appendToken, './api/prfiles', url)),
166
+ fetchPrCommits: async (url) =>
167
+ parseCommits(await requestJson(appendToken, './api/prcommits', url)),
168
+ fetchRelatedPrs: async (url) =>
169
+ parseRelatedPrs(await requestJson(appendToken, './api/relatedprs', url)),
170
+ fetchIssueState: async (url) =>
171
+ parseState(await requestJson(appendToken, './api/issuetitle', url)),
172
+ });
173
+
174
+ export const postConsoleOperation = async (
175
+ appendToken: AppendToken,
176
+ apiPath: string,
177
+ body: ConsoleReviewRequest | ConsoleTriageRequest | ConsoleIntmuxRequest,
178
+ ): Promise<void> => {
179
+ const response = await fetch(appendToken(apiPath), {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify(body),
183
+ });
184
+ if (!response.ok) {
185
+ throw new Error(`HTTP ${response.status}`);
186
+ }
187
+ };
@@ -0,0 +1,76 @@
1
+ import {
2
+ type ConsoleMarkdownSegment,
3
+ hasMermaidFence,
4
+ renderMarkdownToSafeHtml,
5
+ splitMarkdownSegments,
6
+ } from './markdown';
7
+
8
+ const stripKey = (segment: ConsoleMarkdownSegment): Record<string, unknown> => {
9
+ if (segment.kind === 'mermaid') {
10
+ return { kind: segment.kind, code: segment.code };
11
+ }
12
+ return { kind: segment.kind, source: segment.source };
13
+ };
14
+
15
+ describe('renderMarkdownToSafeHtml', () => {
16
+ it('renders headings and lists', () => {
17
+ const html = renderMarkdownToSafeHtml('# Title\n\n- one\n- two');
18
+ expect(html).toContain('<h1');
19
+ expect(html).toContain('<li>one</li>');
20
+ });
21
+
22
+ it('strips script tags via DOMPurify', () => {
23
+ const html = renderMarkdownToSafeHtml(
24
+ 'before <script>alert(1)</script> after',
25
+ );
26
+ expect(html).not.toContain('<script>');
27
+ expect(html).not.toContain('alert(1)');
28
+ });
29
+
30
+ it('strips event-handler attributes', () => {
31
+ const html = renderMarkdownToSafeHtml('<img src="x" onerror="alert(1)">');
32
+ expect(html).not.toContain('onerror');
33
+ });
34
+
35
+ it('returns an empty string for blank input', () => {
36
+ expect(renderMarkdownToSafeHtml(' ')).toBe('');
37
+ });
38
+ });
39
+
40
+ describe('splitMarkdownSegments', () => {
41
+ it('separates a mermaid fence from surrounding markdown', () => {
42
+ const segments = splitMarkdownSegments(
43
+ 'intro\n\n```mermaid\ngraph TD; A-->B;\n```\n\noutro',
44
+ );
45
+ expect(segments.map(stripKey)).toEqual([
46
+ { kind: 'markdown', source: 'intro\n' },
47
+ { kind: 'mermaid', code: 'graph TD; A-->B;' },
48
+ { kind: 'markdown', source: '\noutro' },
49
+ ]);
50
+ expect(new Set(segments.map((segment) => segment.key)).size).toBe(3);
51
+ });
52
+
53
+ it('keeps plain markdown as a single segment', () => {
54
+ const segments = splitMarkdownSegments('just text');
55
+ expect(segments.map(stripKey)).toEqual([
56
+ { kind: 'markdown', source: 'just text' },
57
+ ]);
58
+ });
59
+
60
+ it('keeps an unterminated mermaid fence as markdown', () => {
61
+ const segments = splitMarkdownSegments('```mermaid\ngraph TD; A-->B;');
62
+ expect(segments.map(stripKey)).toEqual([
63
+ { kind: 'markdown', source: '```mermaid\ngraph TD; A-->B;' },
64
+ ]);
65
+ });
66
+ });
67
+
68
+ describe('hasMermaidFence', () => {
69
+ it('detects a mermaid fence', () => {
70
+ expect(hasMermaidFence('```mermaid\ngraph TD; A-->B;\n```')).toBe(true);
71
+ });
72
+
73
+ it('returns false without a mermaid fence', () => {
74
+ expect(hasMermaidFence('```ts\nconst a = 1;\n```')).toBe(false);
75
+ });
76
+ });
@@ -0,0 +1,73 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { marked } from 'marked';
3
+
4
+ export const renderMarkdownToSafeHtml = (source: string): string => {
5
+ const trimmed = source.trim();
6
+ if (trimmed === '') {
7
+ return '';
8
+ }
9
+ marked.setOptions({ gfm: true, breaks: true });
10
+ const parsed = marked.parse(source, { async: false });
11
+ const rawHtml = typeof parsed === 'string' ? parsed : '';
12
+ return DOMPurify.sanitize(rawHtml);
13
+ };
14
+
15
+ export type ConsoleMarkdownSegment =
16
+ | { kind: 'markdown'; key: string; source: string }
17
+ | { kind: 'mermaid'; key: string; code: string };
18
+
19
+ const MERMAID_FENCE = /^```mermaid[^\n]*\n([\s\S]*?)\n```$/;
20
+
21
+ export const splitMarkdownSegments = (
22
+ source: string,
23
+ ): ConsoleMarkdownSegment[] => {
24
+ const lines = source.split('\n');
25
+ const segments: ConsoleMarkdownSegment[] = [];
26
+ let markdownBuffer: string[] = [];
27
+ let mermaidBuffer: string[] | null = null;
28
+ let sequence = 0;
29
+
30
+ const flushMarkdown = (): void => {
31
+ if (markdownBuffer.length > 0) {
32
+ segments.push({
33
+ kind: 'markdown',
34
+ key: `markdown:${sequence}`,
35
+ source: markdownBuffer.join('\n'),
36
+ });
37
+ sequence += 1;
38
+ markdownBuffer = [];
39
+ }
40
+ };
41
+
42
+ for (const line of lines) {
43
+ if (mermaidBuffer === null && /^```mermaid\s*$/.test(line.trim())) {
44
+ flushMarkdown();
45
+ mermaidBuffer = [];
46
+ continue;
47
+ }
48
+ if (mermaidBuffer !== null) {
49
+ if (line.trim() === '```') {
50
+ segments.push({
51
+ kind: 'mermaid',
52
+ key: `mermaid:${sequence}`,
53
+ code: mermaidBuffer.join('\n'),
54
+ });
55
+ sequence += 1;
56
+ mermaidBuffer = null;
57
+ continue;
58
+ }
59
+ mermaidBuffer.push(line);
60
+ continue;
61
+ }
62
+ markdownBuffer.push(line);
63
+ }
64
+
65
+ if (mermaidBuffer !== null) {
66
+ markdownBuffer.push('```mermaid', ...mermaidBuffer);
67
+ }
68
+ flushMarkdown();
69
+ return segments;
70
+ };
71
+
72
+ export const hasMermaidFence = (source: string): boolean =>
73
+ /```mermaid\s*\n/.test(source) || MERMAID_FENCE.test(source.trim());
@@ -0,0 +1,27 @@
1
+ import { renderMermaidToSvg } from './mermaidLoader';
2
+
3
+ const initialize = jest.fn();
4
+ const render = jest.fn(async () => ({
5
+ svg: '<svg><script>alert(1)</script><g></g></svg>',
6
+ }));
7
+
8
+ describe('renderMermaidToSvg', () => {
9
+ beforeEach(() => {
10
+ initialize.mockClear();
11
+ render.mockClear();
12
+ (window as unknown as { mermaid: unknown }).mermaid = {
13
+ initialize,
14
+ render,
15
+ };
16
+ });
17
+
18
+ it('initializes mermaid once and returns sanitized svg without scripts', async () => {
19
+ const first = await renderMermaidToSvg('graph TD; A-->B;');
20
+ const second = await renderMermaidToSvg('graph TD; C-->D;');
21
+ expect(initialize).toHaveBeenCalledTimes(1);
22
+ expect(render).toHaveBeenCalledTimes(2);
23
+ expect(first).not.toContain('<script>');
24
+ expect(first).toContain('<svg');
25
+ expect(second).not.toContain('alert(1)');
26
+ });
27
+ });
@@ -0,0 +1,71 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ type MermaidModule = {
4
+ initialize: (config: Record<string, unknown>) => void;
5
+ render: (id: string, code: string) => Promise<{ svg: string }>;
6
+ };
7
+
8
+ declare global {
9
+ interface Window {
10
+ mermaid?: MermaidModule;
11
+ }
12
+ }
13
+
14
+ export const MERMAID_SCRIPT_URL =
15
+ 'https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js';
16
+
17
+ let mermaidPromise: Promise<MermaidModule> | null = null;
18
+ let renderSequence = 0;
19
+
20
+ const initializeMermaid = (mermaid: MermaidModule): MermaidModule => {
21
+ mermaid.initialize({
22
+ startOnLoad: false,
23
+ securityLevel: 'strict',
24
+ theme: 'dark',
25
+ themeVariables: {
26
+ background: '#0d1117',
27
+ primaryColor: '#161b22',
28
+ primaryTextColor: '#e6edf3',
29
+ primaryBorderColor: '#30363d',
30
+ lineColor: '#8b949e',
31
+ fontSize: '14px',
32
+ },
33
+ });
34
+ return mermaid;
35
+ };
36
+
37
+ const loadMermaid = (): Promise<MermaidModule> => {
38
+ if (mermaidPromise !== null) {
39
+ return mermaidPromise;
40
+ }
41
+ if (window.mermaid !== undefined) {
42
+ mermaidPromise = Promise.resolve(initializeMermaid(window.mermaid));
43
+ return mermaidPromise;
44
+ }
45
+ mermaidPromise = new Promise<MermaidModule>((resolve, reject) => {
46
+ const script = document.createElement('script');
47
+ script.src = MERMAID_SCRIPT_URL;
48
+ script.async = true;
49
+ script.onload = () => {
50
+ if (window.mermaid === undefined) {
51
+ reject(new Error('mermaid failed to load'));
52
+ return;
53
+ }
54
+ resolve(initializeMermaid(window.mermaid));
55
+ };
56
+ script.onerror = () => reject(new Error('mermaid script failed to load'));
57
+ document.head.appendChild(script);
58
+ });
59
+ return mermaidPromise;
60
+ };
61
+
62
+ export const renderMermaidToSvg = async (code: string): Promise<string> => {
63
+ const mermaid = await loadMermaid();
64
+ renderSequence += 1;
65
+ const id = `console-mermaid-${renderSequence}`;
66
+ const { svg } = await mermaid.render(id, code);
67
+ return DOMPurify.sanitize(svg, {
68
+ USE_PROFILES: { svg: true, svgFilters: true },
69
+ ADD_TAGS: ['foreignObject'],
70
+ });
71
+ };
@@ -0,0 +1,56 @@
1
+ import { ResourceCache, runWithConcurrency } from './resourceCache';
2
+
3
+ describe('ResourceCache', () => {
4
+ it('fetches once and caches the result', async () => {
5
+ const fetcher = jest.fn(async (url: string) => `value:${url}`);
6
+ const cache = new ResourceCache(fetcher);
7
+ expect(await cache.load('k', 'u')).toBe('value:u');
8
+ expect(await cache.load('k', 'u')).toBe('value:u');
9
+ expect(fetcher).toHaveBeenCalledTimes(1);
10
+ expect(cache.peek('k')).toBe('value:u');
11
+ });
12
+
13
+ it('de-duplicates concurrent in-flight requests', async () => {
14
+ let resolveFetch: (value: string) => void = () => {};
15
+ const fetcher = jest.fn(
16
+ () =>
17
+ new Promise<string>((resolve) => {
18
+ resolveFetch = resolve;
19
+ }),
20
+ );
21
+ const cache = new ResourceCache(fetcher);
22
+ const first = cache.load('k', 'u');
23
+ const second = cache.load('k', 'u');
24
+ resolveFetch('shared');
25
+ expect(await first).toBe('shared');
26
+ expect(await second).toBe('shared');
27
+ expect(fetcher).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it('allows a retry after a failed fetch', async () => {
31
+ const fetcher = jest
32
+ .fn<Promise<string>, [string]>()
33
+ .mockRejectedValueOnce(new Error('boom'))
34
+ .mockResolvedValueOnce('ok');
35
+ const cache = new ResourceCache(fetcher);
36
+ await expect(cache.load('k', 'u')).rejects.toThrow('boom');
37
+ expect(await cache.load('k', 'u')).toBe('ok');
38
+ expect(fetcher).toHaveBeenCalledTimes(2);
39
+ });
40
+ });
41
+
42
+ describe('runWithConcurrency', () => {
43
+ it('never exceeds the concurrency limit', async () => {
44
+ let active = 0;
45
+ let maxActive = 0;
46
+ const makeTask = () => async () => {
47
+ active += 1;
48
+ maxActive = Math.max(maxActive, active);
49
+ await new Promise((resolve) => setTimeout(resolve, 5));
50
+ active -= 1;
51
+ };
52
+ const tasks = Array.from({ length: 6 }, makeTask);
53
+ await runWithConcurrency(tasks, 2);
54
+ expect(maxActive).toBeLessThanOrEqual(2);
55
+ });
56
+ });
@@ -0,0 +1,51 @@
1
+ export type ResourceFetcher<T> = (url: string) => Promise<T>;
2
+
3
+ export class ResourceCache<T> {
4
+ private readonly cache = new Map<string, T>();
5
+ private readonly inFlight = new Map<string, Promise<T>>();
6
+
7
+ constructor(private readonly fetcher: ResourceFetcher<T>) {}
8
+
9
+ peek = (key: string): T | undefined => this.cache.get(key);
10
+
11
+ has = (key: string): boolean => this.cache.has(key);
12
+
13
+ load = (key: string, url: string): Promise<T> => {
14
+ const cached = this.cache.get(key);
15
+ if (cached !== undefined) {
16
+ return Promise.resolve(cached);
17
+ }
18
+ const pending = this.inFlight.get(key);
19
+ if (pending !== undefined) {
20
+ return pending;
21
+ }
22
+ const promise = this.fetcher(url)
23
+ .then((value) => {
24
+ this.cache.set(key, value);
25
+ return value;
26
+ })
27
+ .finally(() => {
28
+ this.inFlight.delete(key);
29
+ });
30
+ this.inFlight.set(key, promise);
31
+ return promise;
32
+ };
33
+ }
34
+
35
+ export const runWithConcurrency = async (
36
+ tasks: (() => Promise<unknown>)[],
37
+ limit: number,
38
+ ): Promise<void> => {
39
+ let index = 0;
40
+ const workers = Array.from(
41
+ { length: Math.min(limit, tasks.length) },
42
+ async () => {
43
+ while (index < tasks.length) {
44
+ const current = tasks[index];
45
+ index += 1;
46
+ await current();
47
+ }
48
+ },
49
+ );
50
+ await Promise.all(workers);
51
+ };
@@ -0,0 +1,34 @@
1
+ import { CONSOLE_COLOR_PALETTE, colorFromEnum } from './colors';
2
+
3
+ describe('colorFromEnum', () => {
4
+ it('returns the exact palette entry for a known enum', () => {
5
+ expect(colorFromEnum('GREEN')).toEqual({
6
+ dot: '#3fb950',
7
+ bg: 'rgba(46,160,67,0.15)',
8
+ fg: '#3fb950',
9
+ border: 'rgba(46,160,67,0.4)',
10
+ });
11
+ });
12
+
13
+ it('is case insensitive', () => {
14
+ expect(colorFromEnum('purple')).toBe(CONSOLE_COLOR_PALETTE.PURPLE);
15
+ });
16
+
17
+ it('falls back to GRAY for an unknown enum', () => {
18
+ expect(colorFromEnum('MAGENTA')).toBe(CONSOLE_COLOR_PALETTE.GRAY);
19
+ });
20
+
21
+ it('falls back to GRAY for null', () => {
22
+ expect(colorFromEnum(null)).toBe(CONSOLE_COLOR_PALETTE.GRAY);
23
+ });
24
+
25
+ it('uses the documented dot colors for every enum', () => {
26
+ expect(CONSOLE_COLOR_PALETTE.GRAY.dot).toBe('#848d97');
27
+ expect(CONSOLE_COLOR_PALETTE.BLUE.dot).toBe('#4493f8');
28
+ expect(CONSOLE_COLOR_PALETTE.YELLOW.dot).toBe('#d29922');
29
+ expect(CONSOLE_COLOR_PALETTE.ORANGE.dot).toBe('#db6d28');
30
+ expect(CONSOLE_COLOR_PALETTE.RED.dot).toBe('#f85149');
31
+ expect(CONSOLE_COLOR_PALETTE.PINK.dot).toBe('#db61a2');
32
+ expect(CONSOLE_COLOR_PALETTE.PURPLE.dot).toBe('#a371f7');
33
+ });
34
+ });
@@ -0,0 +1,73 @@
1
+ import type { ConsoleColor } from './types';
2
+
3
+ export type ConsolePaletteEntry = {
4
+ dot: string;
5
+ bg: string;
6
+ fg: string;
7
+ border: string;
8
+ };
9
+
10
+ export const CONSOLE_COLOR_PALETTE: Record<ConsoleColor, ConsolePaletteEntry> =
11
+ {
12
+ GRAY: {
13
+ dot: '#848d97',
14
+ bg: 'rgba(110,118,129,0.1)',
15
+ fg: '#8b949e',
16
+ border: 'rgba(110,118,129,0.4)',
17
+ },
18
+ BLUE: {
19
+ dot: '#4493f8',
20
+ bg: 'rgba(56,139,253,0.1)',
21
+ fg: '#388bfd',
22
+ border: 'rgba(56,139,253,0.4)',
23
+ },
24
+ GREEN: {
25
+ dot: '#3fb950',
26
+ bg: 'rgba(46,160,67,0.15)',
27
+ fg: '#3fb950',
28
+ border: 'rgba(46,160,67,0.4)',
29
+ },
30
+ YELLOW: {
31
+ dot: '#d29922',
32
+ bg: 'rgba(187,128,9,0.15)',
33
+ fg: '#d29922',
34
+ border: 'rgba(187,128,9,0.4)',
35
+ },
36
+ ORANGE: {
37
+ dot: '#db6d28',
38
+ bg: 'rgba(219,109,40,0.1)',
39
+ fg: '#db6d28',
40
+ border: 'rgba(219,109,40,0.4)',
41
+ },
42
+ RED: {
43
+ dot: '#f85149',
44
+ bg: 'rgba(248,81,73,0.1)',
45
+ fg: '#f85149',
46
+ border: 'rgba(248,81,73,0.4)',
47
+ },
48
+ PINK: {
49
+ dot: '#db61a2',
50
+ bg: 'rgba(219,97,162,0.1)',
51
+ fg: '#db61a2',
52
+ border: 'rgba(219,97,162,0.4)',
53
+ },
54
+ PURPLE: {
55
+ dot: '#a371f7',
56
+ bg: 'rgba(163,113,247,0.1)',
57
+ fg: '#a371f7',
58
+ border: 'rgba(163,113,247,0.4)',
59
+ },
60
+ };
61
+
62
+ export const colorFromEnum = (
63
+ colorEnum: string | null,
64
+ ): ConsolePaletteEntry => {
65
+ if (colorEnum === null) {
66
+ return CONSOLE_COLOR_PALETTE.GRAY;
67
+ }
68
+ const key = colorEnum.toUpperCase();
69
+ if (key in CONSOLE_COLOR_PALETTE) {
70
+ return CONSOLE_COLOR_PALETTE[key as ConsoleColor];
71
+ }
72
+ return CONSOLE_COLOR_PALETTE.GRAY;
73
+ };
@@ -0,0 +1,35 @@
1
+ import { fileStatusBadge } from './fileStatus';
2
+
3
+ describe('fileStatusBadge', () => {
4
+ it('maps added to a green A badge', () => {
5
+ expect(fileStatusBadge('added')).toEqual({ label: 'A', color: '#3fb950' });
6
+ });
7
+
8
+ it('maps modified to a yellow M badge', () => {
9
+ expect(fileStatusBadge('modified')).toEqual({
10
+ label: 'M',
11
+ color: '#d29922',
12
+ });
13
+ });
14
+
15
+ it('maps removed to a red D badge', () => {
16
+ expect(fileStatusBadge('removed')).toEqual({
17
+ label: 'D',
18
+ color: '#f85149',
19
+ });
20
+ });
21
+
22
+ it('maps renamed to a purple R badge', () => {
23
+ expect(fileStatusBadge('renamed')).toEqual({
24
+ label: 'R',
25
+ color: '#a371f7',
26
+ });
27
+ });
28
+
29
+ it('maps an unknown status to a gray question badge', () => {
30
+ expect(fileStatusBadge('whatever')).toEqual({
31
+ label: '?',
32
+ color: '#8b949e',
33
+ });
34
+ });
35
+ });