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,32 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsolePanel } from './ConsolePanel';
3
+
4
+ describe('ConsolePanel', () => {
5
+ it('renders expanded by default', () => {
6
+ const { getByText } = render(
7
+ <ConsolePanel title="Description">
8
+ <p>content</p>
9
+ </ConsolePanel>,
10
+ );
11
+ expect(getByText('content')).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders collapsed when defaultCollapsed is set', () => {
15
+ const { queryByText } = render(
16
+ <ConsolePanel title="Commits" defaultCollapsed>
17
+ <p>content</p>
18
+ </ConsolePanel>,
19
+ );
20
+ expect(queryByText('content')).toBeNull();
21
+ });
22
+
23
+ it('toggles collapsed state on the header button', () => {
24
+ const { getByRole, queryByText } = render(
25
+ <ConsolePanel title="Comments" defaultCollapsed>
26
+ <p>content</p>
27
+ </ConsolePanel>,
28
+ );
29
+ fireEvent.click(getByRole('button'));
30
+ expect(queryByText('content')).not.toBeNull();
31
+ });
32
+ });
@@ -0,0 +1,36 @@
1
+ import { type ReactNode, useState } from 'react';
2
+
3
+ export type ConsolePanelProps = {
4
+ title: string;
5
+ defaultCollapsed?: boolean;
6
+ headerAction?: ReactNode;
7
+ children: ReactNode;
8
+ };
9
+
10
+ export const ConsolePanel = ({
11
+ title,
12
+ defaultCollapsed = false,
13
+ headerAction,
14
+ children,
15
+ }: ConsolePanelProps) => {
16
+ const [collapsed, setCollapsed] = useState<boolean>(defaultCollapsed);
17
+ return (
18
+ <section className="console-panel">
19
+ <header className="console-panel-header">
20
+ <button
21
+ type="button"
22
+ className="console-panel-toggle"
23
+ aria-expanded={!collapsed}
24
+ onClick={() => setCollapsed((value) => !value)}
25
+ >
26
+ <span className="console-panel-caret">{collapsed ? '▸' : '▾'}</span>
27
+ <span className="console-panel-title">{title}</span>
28
+ </button>
29
+ {headerAction !== undefined && (
30
+ <div className="console-panel-action">{headerAction}</div>
31
+ )}
32
+ </header>
33
+ {!collapsed && <div className="console-panel-body">{children}</div>}
34
+ </section>
35
+ );
36
+ };
@@ -0,0 +1,29 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleProjectHeader } from './ConsoleProjectHeader';
3
+
4
+ const meta: Meta<typeof ConsoleProjectHeader> = {
5
+ title: 'Console/ConsoleProjectHeader',
6
+ component: ConsoleProjectHeader,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsoleProjectHeader>;
12
+
13
+ export const WithProject: Story = {
14
+ args: {
15
+ pjcode: 'umino',
16
+ },
17
+ };
18
+
19
+ export const AnotherProject: Story = {
20
+ args: {
21
+ pjcode: 'xmile',
22
+ },
23
+ };
24
+
25
+ export const NoProject: Story = {
26
+ args: {
27
+ pjcode: null,
28
+ },
29
+ };
@@ -0,0 +1,14 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsoleProjectHeader } from './ConsoleProjectHeader';
3
+
4
+ describe('ConsoleProjectHeader', () => {
5
+ it('shows the active project code when a pjcode is provided', () => {
6
+ const { getByText } = render(<ConsoleProjectHeader pjcode="umino" />);
7
+ expect(getByText('project: umino')).toBeInTheDocument();
8
+ });
9
+
10
+ it('shows a no-project message when the pjcode is null', () => {
11
+ const { getByText } = render(<ConsoleProjectHeader pjcode={null} />);
12
+ expect(getByText('no project selected')).toBeInTheDocument();
13
+ });
14
+ });
@@ -0,0 +1,14 @@
1
+ export type ConsoleProjectHeaderProps = {
2
+ pjcode: string | null;
3
+ };
4
+
5
+ export const ConsoleProjectHeader = ({ pjcode }: ConsoleProjectHeaderProps) => (
6
+ <header className="flex items-baseline gap-2 border-b border-border p-3">
7
+ <h1 className="text-base font-semibold">TDPM Console</h1>
8
+ {pjcode === null ? (
9
+ <span className="text-sm text-muted-foreground">no project selected</span>
10
+ ) : (
11
+ <span className="text-sm text-muted-foreground">project: {pjcode}</span>
12
+ )}
13
+ </header>
14
+ );
@@ -0,0 +1,14 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsolePullRequestReviewGroup } from './ConsolePullRequestReviewGroup';
3
+
4
+ const meta: Meta<typeof ConsolePullRequestReviewGroup> = {
5
+ title: 'Console/ConsolePullRequestReviewGroup',
6
+ component: ConsolePullRequestReviewGroup,
7
+ args: { onReview: () => {} },
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsolePullRequestReviewGroup>;
13
+
14
+ export const Default: Story = {};
@@ -0,0 +1,33 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsolePullRequestReviewGroup } from './ConsolePullRequestReviewGroup';
3
+
4
+ describe('ConsolePullRequestReviewGroup', () => {
5
+ it('renders the four buttons left to right', () => {
6
+ const { getAllByRole } = render(
7
+ <ConsolePullRequestReviewGroup onReview={() => {}} />,
8
+ );
9
+ expect(getAllByRole('button').map((button) => button.textContent)).toEqual([
10
+ 'Unnecessary',
11
+ 'Totally wrong',
12
+ 'Reject',
13
+ 'Approve',
14
+ ]);
15
+ });
16
+
17
+ it('reports each review action', () => {
18
+ const onReview = jest.fn();
19
+ const { getByText } = render(
20
+ <ConsolePullRequestReviewGroup onReview={onReview} />,
21
+ );
22
+ fireEvent.click(getByText('Unnecessary'));
23
+ fireEvent.click(getByText('Totally wrong'));
24
+ fireEvent.click(getByText('Reject'));
25
+ fireEvent.click(getByText('Approve'));
26
+ expect(onReview.mock.calls.map((call) => call[0])).toEqual([
27
+ 'unnecessary',
28
+ 'totally_wrong',
29
+ 'request_changes',
30
+ 'approve',
31
+ ]);
32
+ });
33
+ });
@@ -0,0 +1,34 @@
1
+ import type { ConsoleReviewAction } from '../operations';
2
+
3
+ export type ConsolePullRequestReviewGroupProps = {
4
+ onReview: (action: ConsoleReviewAction) => void;
5
+ };
6
+
7
+ const REVIEW_BUTTONS: {
8
+ action: ConsoleReviewAction;
9
+ label: string;
10
+ color: string;
11
+ }[] = [
12
+ { action: 'unnecessary', label: 'Unnecessary', color: '#8b949e' },
13
+ { action: 'totally_wrong', label: 'Totally wrong', color: '#f85149' },
14
+ { action: 'request_changes', label: 'Reject', color: '#d29922' },
15
+ { action: 'approve', label: 'Approve', color: '#3fb950' },
16
+ ];
17
+
18
+ export const ConsolePullRequestReviewGroup = ({
19
+ onReview,
20
+ }: ConsolePullRequestReviewGroupProps) => (
21
+ <div className="console-op-group">
22
+ {REVIEW_BUTTONS.map((button) => (
23
+ <button
24
+ key={button.action}
25
+ type="button"
26
+ className="console-op-button"
27
+ style={{ color: button.color, borderColor: button.color }}
28
+ onClick={() => onReview(button.action)}
29
+ >
30
+ {button.label}
31
+ </button>
32
+ ))}
33
+ </div>
34
+ );
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ consoleChangedFilesFixture,
4
+ consoleCommitsFixture,
5
+ consoleRelatedPullRequestsFixture,
6
+ } from '../fixtures';
7
+ import { ConsolePullRequestSection } from './ConsolePullRequestSection';
8
+
9
+ const meta: Meta<typeof ConsolePullRequestSection> = {
10
+ title: 'Console/ConsolePullRequestSection',
11
+ component: ConsolePullRequestSection,
12
+ args: { now: Date.parse('2026-06-19T12:00:00.000Z') },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof ConsolePullRequestSection>;
18
+
19
+ export const WithDetail: Story = {
20
+ args: {
21
+ pullRequest: consoleRelatedPullRequestsFixture[0],
22
+ body: consoleRelatedPullRequestsFixture[0].summary?.body ?? '',
23
+ bodyIsLoading: false,
24
+ files: consoleChangedFilesFixture,
25
+ filesAreLoading: false,
26
+ filesError: null,
27
+ commits: consoleCommitsFixture,
28
+ commitsAreLoading: false,
29
+ commitsError: null,
30
+ },
31
+ };
@@ -0,0 +1,40 @@
1
+ import { render } from '@testing-library/react';
2
+ import {
3
+ consoleChangedFilesFixture,
4
+ consoleCommitsFixture,
5
+ consoleRelatedPullRequestsFixture,
6
+ } from '../fixtures';
7
+ import { ConsolePullRequestSection } from './ConsolePullRequestSection';
8
+
9
+ jest.mock('../lib/mermaidLoader', () => ({
10
+ renderMermaidToSvg: jest.fn(async () => '<svg></svg>'),
11
+ }));
12
+
13
+ const now = Date.parse('2026-06-19T12:00:00.000Z');
14
+ const pullRequest = consoleRelatedPullRequestsFixture[0];
15
+
16
+ describe('ConsolePullRequestSection', () => {
17
+ it('renders the title, stat bar and changed files', () => {
18
+ const { getByText } = render(
19
+ <ConsolePullRequestSection
20
+ pullRequest={pullRequest}
21
+ body={pullRequest.summary?.body ?? ''}
22
+ bodyIsLoading={false}
23
+ files={consoleChangedFilesFixture}
24
+ filesAreLoading={false}
25
+ filesError={null}
26
+ commits={consoleCommitsFixture}
27
+ commitsAreLoading={false}
28
+ commitsError={null}
29
+ now={now}
30
+ />,
31
+ );
32
+ expect(
33
+ getByText(
34
+ 'Scaffold React console UI under entry-points with build bundling',
35
+ ),
36
+ ).toBeInTheDocument();
37
+ expect(getByText('+1184')).toBeInTheDocument();
38
+ expect(getByText('27 files')).toBeInTheDocument();
39
+ });
40
+ });
@@ -0,0 +1,88 @@
1
+ import type {
2
+ ConsoleChangedFile,
3
+ ConsoleCommit,
4
+ ConsoleRelatedPullRequest,
5
+ } from '../types';
6
+ import { ConsoleChangedFileList } from './ConsoleChangedFileList';
7
+ import { ConsoleCommitList } from './ConsoleCommitList';
8
+ import { ConsoleMarkdownView } from './ConsoleMarkdownView';
9
+ import { ConsolePanel } from './ConsolePanel';
10
+
11
+ export type ConsolePullRequestSectionProps = {
12
+ pullRequest: ConsoleRelatedPullRequest;
13
+ body: string;
14
+ bodyIsLoading: boolean;
15
+ files: ConsoleChangedFile[];
16
+ filesAreLoading: boolean;
17
+ filesError: string | null;
18
+ commits: ConsoleCommit[];
19
+ commitsAreLoading: boolean;
20
+ commitsError: string | null;
21
+ now: number;
22
+ };
23
+
24
+ export const ConsolePullRequestSection = ({
25
+ pullRequest,
26
+ body,
27
+ bodyIsLoading,
28
+ files,
29
+ filesAreLoading,
30
+ filesError,
31
+ commits,
32
+ commitsAreLoading,
33
+ commitsError,
34
+ now,
35
+ }: ConsolePullRequestSectionProps) => {
36
+ const summary = pullRequest.summary;
37
+ return (
38
+ <section className="console-pr-section">
39
+ <header className="console-pr-section-header">
40
+ <a
41
+ href={pullRequest.url}
42
+ className="console-pr-section-title"
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ >
46
+ {summary?.title ?? pullRequest.url}
47
+ </a>
48
+ {pullRequest.isDraft && (
49
+ <span className="console-pr-section-state">draft</span>
50
+ )}
51
+ </header>
52
+ <div className="console-pr-statbar">
53
+ {pullRequest.branchName !== null && (
54
+ <span className="console-pr-branch">{pullRequest.branchName}</span>
55
+ )}
56
+ {summary !== null && (
57
+ <>
58
+ <span className="console-pr-add">+{summary.additions}</span>
59
+ <span className="console-pr-del">-{summary.deletions}</span>
60
+ <span className="console-pr-files-count">
61
+ {summary.changedFiles} files
62
+ </span>
63
+ </>
64
+ )}
65
+ </div>
66
+ {bodyIsLoading ? (
67
+ <p className="console-pr-body-loading">Loading description...</p>
68
+ ) : (
69
+ <ConsoleMarkdownView body={summary?.body ?? body} />
70
+ )}
71
+ <ConsolePanel title="Changed files">
72
+ <ConsoleChangedFileList
73
+ files={files}
74
+ isLoading={filesAreLoading}
75
+ error={filesError}
76
+ />
77
+ </ConsolePanel>
78
+ <ConsolePanel title="Commits" defaultCollapsed>
79
+ <ConsoleCommitList
80
+ commits={commits}
81
+ isLoading={commitsAreLoading}
82
+ error={commitsError}
83
+ now={now}
84
+ />
85
+ </ConsolePanel>
86
+ </section>
87
+ );
88
+ };
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleStatusOptionsFixture } from '../fixtures';
3
+ import { ConsoleStatusButtonGroup } from './ConsoleStatusButtonGroup';
4
+
5
+ const meta: Meta<typeof ConsoleStatusButtonGroup> = {
6
+ title: 'Console/ConsoleStatusButtonGroup',
7
+ component: ConsoleStatusButtonGroup,
8
+ args: { onSetStatus: () => {}, onSetInTmuxByHuman: () => {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof ConsoleStatusButtonGroup>;
14
+
15
+ export const AllStatusOptions: Story = {
16
+ args: { statusOptions: consoleStatusOptionsFixture },
17
+ };
@@ -0,0 +1,49 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { consoleStatusOptionsFixture } from '../fixtures';
3
+ import { ConsoleStatusButtonGroup } from './ConsoleStatusButtonGroup';
4
+
5
+ describe('ConsoleStatusButtonGroup', () => {
6
+ it('renders only the four routed status buttons that exist in the data', () => {
7
+ const { getByText, queryByText } = render(
8
+ <ConsoleStatusButtonGroup
9
+ statusOptions={consoleStatusOptionsFixture}
10
+ onSetStatus={() => {}}
11
+ onSetInTmuxByHuman={() => {}}
12
+ />,
13
+ );
14
+ expect(getByText('In Tmux by agent')).toBeInTheDocument();
15
+ expect(getByText('In Tmux by human')).toBeInTheDocument();
16
+ expect(getByText('Todo by human')).toBeInTheDocument();
17
+ expect(getByText('Awaiting Workspace')).toBeInTheDocument();
18
+ expect(queryByText('Preparation')).toBeNull();
19
+ });
20
+
21
+ it('routes In Tmux by human to the intmux handler and others to the status handler', () => {
22
+ const onSetStatus = jest.fn();
23
+ const onSetInTmuxByHuman = jest.fn();
24
+ const { getByText } = render(
25
+ <ConsoleStatusButtonGroup
26
+ statusOptions={consoleStatusOptionsFixture}
27
+ onSetStatus={onSetStatus}
28
+ onSetInTmuxByHuman={onSetInTmuxByHuman}
29
+ />,
30
+ );
31
+ fireEvent.click(getByText('In Tmux by human'));
32
+ fireEvent.click(getByText('In Tmux by agent'));
33
+ expect(onSetInTmuxByHuman).toHaveBeenCalledTimes(1);
34
+ expect(onSetInTmuxByHuman.mock.calls[0][0].name).toBe('In Tmux by human');
35
+ expect(onSetStatus).toHaveBeenCalledTimes(1);
36
+ expect(onSetStatus.mock.calls[0][0].name).toBe('In Tmux by agent');
37
+ });
38
+
39
+ it('renders nothing when no routed option exists', () => {
40
+ const { container } = render(
41
+ <ConsoleStatusButtonGroup
42
+ statusOptions={[{ id: 'x', name: 'Preparation', color: 'YELLOW' }]}
43
+ onSetStatus={() => {}}
44
+ onSetInTmuxByHuman={() => {}}
45
+ />,
46
+ );
47
+ expect(container.firstChild).toBeNull();
48
+ });
49
+ });
@@ -0,0 +1,63 @@
1
+ import { colorFromEnum } from '../colors';
2
+ import { IN_TMUX_BY_HUMAN_NAME, STATUS_BUTTON_NAMES } from '../operations';
3
+ import type { ConsoleFieldOption } from '../types';
4
+
5
+ export type ConsoleStatusButtonGroupProps = {
6
+ statusOptions: ConsoleFieldOption[];
7
+ onSetStatus: (option: ConsoleFieldOption) => void;
8
+ onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
9
+ };
10
+
11
+ const findStatusOption = (
12
+ statusOptions: ConsoleFieldOption[],
13
+ name: string,
14
+ ): ConsoleFieldOption | null => {
15
+ const lower = name.toLowerCase();
16
+ return (
17
+ statusOptions.find((option) => option.name.toLowerCase() === lower) ?? null
18
+ );
19
+ };
20
+
21
+ export const ConsoleStatusButtonGroup = ({
22
+ statusOptions,
23
+ onSetStatus,
24
+ onSetInTmuxByHuman,
25
+ }: ConsoleStatusButtonGroupProps) => {
26
+ const buttons = STATUS_BUTTON_NAMES.map((name) => ({
27
+ name,
28
+ option: findStatusOption(statusOptions, name),
29
+ })).filter(
30
+ (entry): entry is { name: string; option: ConsoleFieldOption } =>
31
+ entry.option !== null,
32
+ );
33
+
34
+ if (buttons.length === 0) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <div className="console-op-group">
40
+ {buttons.map(({ name, option }) => {
41
+ const palette = colorFromEnum(option.color);
42
+ const isInTmuxByHuman = name === IN_TMUX_BY_HUMAN_NAME;
43
+ return (
44
+ <button
45
+ key={option.id}
46
+ type="button"
47
+ className="console-op-button"
48
+ style={{
49
+ color: palette.fg,
50
+ borderColor: palette.border,
51
+ backgroundColor: palette.bg,
52
+ }}
53
+ onClick={() =>
54
+ isInTmuxByHuman ? onSetInTmuxByHuman(option) : onSetStatus(option)
55
+ }
56
+ >
57
+ {option.name}
58
+ </button>
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ };
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleStoryOptionsFixture } from '../fixtures';
3
+ import { ConsoleStoryButtonGroup } from './ConsoleStoryButtonGroup';
4
+
5
+ const meta: Meta<typeof ConsoleStoryButtonGroup> = {
6
+ title: 'Console/ConsoleStoryButtonGroup',
7
+ component: ConsoleStoryButtonGroup,
8
+ args: { onSetStory: () => {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof ConsoleStoryButtonGroup>;
14
+
15
+ export const AssignableStories: Story = {
16
+ args: { storyOptions: consoleStoryOptionsFixture },
17
+ };
@@ -0,0 +1,45 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsoleStoryButtonGroup } from './ConsoleStoryButtonGroup';
3
+
4
+ const options = [
5
+ { id: '1', name: 'TDPM Console port', color: 'BLUE' as const },
6
+ {
7
+ id: '2',
8
+ name: "regular / NO STORY; DON'T WORK ON THIS",
9
+ color: 'RED' as const,
10
+ },
11
+ { id: '3', name: 'Move to Okinawa', color: 'PURPLE' as const },
12
+ ];
13
+
14
+ describe('ConsoleStoryButtonGroup', () => {
15
+ it('excludes no-story options', () => {
16
+ const { getByText, queryByText } = render(
17
+ <ConsoleStoryButtonGroup storyOptions={options} onSetStory={() => {}} />,
18
+ );
19
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
20
+ expect(getByText('Move to Okinawa')).toBeInTheDocument();
21
+ expect(queryByText(/NO STORY/)).toBeNull();
22
+ });
23
+
24
+ it('reports the selected story option', () => {
25
+ const onSetStory = jest.fn();
26
+ const { getByText } = render(
27
+ <ConsoleStoryButtonGroup
28
+ storyOptions={options}
29
+ onSetStory={onSetStory}
30
+ />,
31
+ );
32
+ fireEvent.click(getByText('Move to Okinawa'));
33
+ expect(onSetStory).toHaveBeenCalledWith(options[2]);
34
+ });
35
+
36
+ it('renders nothing when only no-story options exist', () => {
37
+ const { container } = render(
38
+ <ConsoleStoryButtonGroup
39
+ storyOptions={[options[1]]}
40
+ onSetStory={() => {}}
41
+ />,
42
+ );
43
+ expect(container.firstChild).toBeNull();
44
+ });
45
+ });
@@ -0,0 +1,42 @@
1
+ import { colorFromEnum } from '../colors';
2
+ import type { ConsoleFieldOption } from '../types';
3
+
4
+ export type ConsoleStoryButtonGroupProps = {
5
+ storyOptions: ConsoleFieldOption[];
6
+ onSetStory: (option: ConsoleFieldOption) => void;
7
+ };
8
+
9
+ const isNoStoryOption = (option: ConsoleFieldOption): boolean =>
10
+ option.name.toLowerCase().includes('no story');
11
+
12
+ export const ConsoleStoryButtonGroup = ({
13
+ storyOptions,
14
+ onSetStory,
15
+ }: ConsoleStoryButtonGroupProps) => {
16
+ const assignable = storyOptions.filter((option) => !isNoStoryOption(option));
17
+ if (assignable.length === 0) {
18
+ return null;
19
+ }
20
+ return (
21
+ <div className="console-op-group console-op-group-stories">
22
+ {assignable.map((option) => {
23
+ const palette = colorFromEnum(option.color);
24
+ return (
25
+ <button
26
+ key={option.id}
27
+ type="button"
28
+ className="console-op-button"
29
+ style={{
30
+ color: palette.fg,
31
+ borderColor: palette.border,
32
+ backgroundColor: palette.bg,
33
+ }}
34
+ onClick={() => onSetStory(option)}
35
+ >
36
+ {option.name}
37
+ </button>
38
+ );
39
+ })}
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleStoryGroupHeader } from './ConsoleStoryGroupHeader';
3
+
4
+ const meta: Meta<typeof ConsoleStoryGroupHeader> = {
5
+ title: 'Console/ConsoleStoryGroupHeader',
6
+ component: ConsoleStoryGroupHeader,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsoleStoryGroupHeader>;
12
+
13
+ export const ConsolePortStory: Story = {
14
+ args: { story: 'TDPM Console port', count: 4, colorEnum: 'BLUE' },
15
+ };
16
+
17
+ export const RegularWorkflowStory: Story = {
18
+ args: {
19
+ story: 'regular / workflow improvement',
20
+ count: 12,
21
+ colorEnum: 'GRAY',
22
+ },
23
+ };
24
+
25
+ export const NoStory: Story = {
26
+ args: { story: '(No story)', count: 3, colorEnum: null },
27
+ };
@@ -0,0 +1,24 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsoleStoryGroupHeader } from './ConsoleStoryGroupHeader';
3
+
4
+ describe('ConsoleStoryGroupHeader', () => {
5
+ it('renders the story name and count', () => {
6
+ const { getByText } = render(
7
+ <ConsoleStoryGroupHeader
8
+ story="TDPM Console port"
9
+ count={4}
10
+ colorEnum="BLUE"
11
+ />,
12
+ );
13
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
14
+ expect(getByText('4')).toBeInTheDocument();
15
+ });
16
+
17
+ it('applies the dot color from the enum', () => {
18
+ const { container } = render(
19
+ <ConsoleStoryGroupHeader story="s" count={1} colorEnum="GREEN" />,
20
+ );
21
+ const dot = container.querySelector('.console-story-dot');
22
+ expect(dot).toHaveStyle({ backgroundColor: '#3fb950' });
23
+ });
24
+ });