github-issue-tower-defence-management 1.191.0 → 1.192.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-B22eMy23.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-RaFqjB7u.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.191.0",
3
+ "version": "1.192.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -291,7 +291,7 @@ test('opens the comment input with the item detail, keeps it on screen while the
291
291
  await page
292
292
  .locator('.console-composer-input')
293
293
  .fill('The dock must not grow with every comment.');
294
- await page.locator('.console-composer-submit').click();
294
+ await page.getByRole('button', { name: 'Comment', exact: true }).click();
295
295
 
296
296
  const postedComment = page.locator('.console-comment', {
297
297
  hasText: 'The dock must not grow with every comment.',
@@ -589,3 +589,37 @@ test('reorders stories on the triage tab with up/down buttons', async ({
589
589
  await expect(rows.nth(0)).toContainText('TDPM Console port');
590
590
  await expect(rows.nth(1)).toContainText('Publish product documentation site');
591
591
  });
592
+
593
+ test('posts a comment and moves the item to Awaiting Workspace when the Comment & Awaiting Workspace button is clicked', async ({
594
+ page,
595
+ }) => {
596
+ await page.goto(harness.appRootUrl);
597
+
598
+ await itemRowByText(
599
+ page,
600
+ 'Resolve the shared GitHub token rate-limit exhaustion blocker',
601
+ ).click();
602
+
603
+ await expect(page.locator('.console-composer-input')).toBeInViewport();
604
+ await page
605
+ .locator('.console-composer-input')
606
+ .fill('handing off to awaiting workspace');
607
+
608
+ await page
609
+ .getByRole('button', { name: 'Comment & Awaiting Workspace', exact: true })
610
+ .click();
611
+
612
+ await expect
613
+ .poll(
614
+ () =>
615
+ harness.commentCalls.some(
616
+ (c) => c.body === 'handing off to awaiting workspace',
617
+ ),
618
+ { timeout: 10000 },
619
+ )
620
+ .toBe(true);
621
+
622
+ await expect(tabByLabel(page, 'Workflow Blocker')).toHaveCount(0, {
623
+ timeout: 8000,
624
+ });
625
+ });
@@ -52,6 +52,11 @@ export type ConsoleE2eStoryColorCall = {
52
52
  newColor: string;
53
53
  };
54
54
 
55
+ export type ConsoleE2eCommentCall = {
56
+ url: string;
57
+ body: string;
58
+ };
59
+
55
60
  type ConsoleFixtureListItem = {
56
61
  number: number;
57
62
  title: string;
@@ -460,6 +465,7 @@ const createStubIssueRepository = (
460
465
  requestChangesCalls: ConsoleE2eRequestChangesCall[],
461
466
  createIssueCalls: ConsoleE2eCreateIssueCall[],
462
467
  storyColorCalls: ConsoleE2eStoryColorCall[],
468
+ commentCalls: ConsoleE2eCommentCall[],
463
469
  ): IssueRepository => ({
464
470
  getAllIssues: () => notImplemented('getAllIssues'),
465
471
  getIssueByUrl: async (url: string): Promise<Issue | null> =>
@@ -563,7 +569,9 @@ const createStubIssueRepository = (
563
569
  closePullRequest: async (): Promise<void> => undefined,
564
570
  closeIssueByUrl: async (): Promise<void> => undefined,
565
571
  deletePullRequestBranch: () => notImplemented('deletePullRequestBranch'),
566
- createCommentByUrl: async (): Promise<void> => undefined,
572
+ createCommentByUrl: async (url: string, body: string): Promise<void> => {
573
+ commentCalls.push({ url, body });
574
+ },
567
575
  getAllOpened: () => notImplemented('getAllOpened'),
568
576
  getStoryObjectMap: () => notImplemented('getStoryObjectMap'),
569
577
  addIssueToProject: async (): Promise<void> => undefined,
@@ -647,6 +655,7 @@ export type ConsoleE2eHarness = {
647
655
  reviewCommentCalls: ConsoleE2eReviewCommentCall[];
648
656
  requestChangesCalls: ConsoleE2eRequestChangesCall[];
649
657
  createIssueCalls: ConsoleE2eCreateIssueCall[];
658
+ commentCalls: ConsoleE2eCommentCall[];
650
659
  reorderStoryCalls: ConsoleE2eReorderStoryCall[];
651
660
  addStoryCalls: ConsoleE2eAddStoryCall[];
652
661
  storyColorCalls: ConsoleE2eStoryColorCall[];
@@ -671,6 +680,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
671
680
  const reviewCommentCalls: ConsoleE2eReviewCommentCall[] = [];
672
681
  const requestChangesCalls: ConsoleE2eRequestChangesCall[] = [];
673
682
  const createIssueCalls: ConsoleE2eCreateIssueCall[] = [];
683
+ const commentCalls: ConsoleE2eCommentCall[] = [];
674
684
  const reorderStoryCalls: ConsoleE2eReorderStoryCall[] = [];
675
685
  const addStoryCalls: ConsoleE2eAddStoryCall[] = [];
676
686
  const storyColorCalls: ConsoleE2eStoryColorCall[] = [];
@@ -684,6 +694,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
684
694
  requestChangesCalls,
685
695
  createIssueCalls,
686
696
  storyColorCalls,
697
+ commentCalls,
687
698
  ),
688
699
  projectRepository: {
689
700
  updateStoryList: async (_updatedProject, stories) => {
@@ -741,6 +752,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
741
752
  reviewCommentCalls,
742
753
  requestChangesCalls,
743
754
  createIssueCalls,
755
+ commentCalls,
744
756
  reorderStoryCalls,
745
757
  addStoryCalls,
746
758
  storyColorCalls,
@@ -51,3 +51,10 @@ export const ComposerWithSavedDraft: Story = {
51
51
  initialDraft: 'Previously typed text…',
52
52
  },
53
53
  };
54
+
55
+ export const ComposerWithMoveToAwaitingWorkspace: Story = {
56
+ args: {
57
+ initiallyOpen: true,
58
+ onSubmitAndMoveToAwaitingWorkspace: acceptComment,
59
+ },
60
+ };
@@ -329,6 +329,88 @@ describe('ConsoleCommentComposer', () => {
329
329
  });
330
330
  });
331
331
 
332
+ it('renders only the Comment button when onSubmitAndMoveToAwaitingWorkspace is not provided', () => {
333
+ const { queryByText, getByText } = render(
334
+ <ConsoleCommentComposer initiallyOpen onSubmit={stubSubmit} />,
335
+ );
336
+ expect(getByText('Comment')).toBeInTheDocument();
337
+ expect(queryByText('Comment & Awaiting Workspace')).toBeNull();
338
+ });
339
+
340
+ it('renders both Comment and Comment & Awaiting Workspace buttons when onSubmitAndMoveToAwaitingWorkspace is provided', () => {
341
+ const { getByText } = render(
342
+ <ConsoleCommentComposer
343
+ initiallyOpen
344
+ onSubmit={stubSubmit}
345
+ onSubmitAndMoveToAwaitingWorkspace={stubSubmit}
346
+ />,
347
+ );
348
+ expect(getByText('Comment')).toBeInTheDocument();
349
+ expect(getByText('Comment & Awaiting Workspace')).toBeInTheDocument();
350
+ });
351
+
352
+ it('calls onSubmit and clears draft when the left Comment button is clicked', async () => {
353
+ const onSubmit = jest.fn(stubSubmit);
354
+ const onSubmitAndMoveToAwaitingWorkspace = jest.fn(stubSubmit);
355
+ const { getByPlaceholderText, getByText } = render(
356
+ <ConsoleCommentComposer
357
+ initiallyOpen
358
+ onSubmit={onSubmit}
359
+ onSubmitAndMoveToAwaitingWorkspace={onSubmitAndMoveToAwaitingWorkspace}
360
+ />,
361
+ );
362
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
363
+ target: { value: 'Left button comment.' },
364
+ });
365
+ fireEvent.click(getByText('Comment'));
366
+ await waitFor(() => {
367
+ expect(getByPlaceholderText('Leave a comment…')).toHaveValue('');
368
+ });
369
+ expect(onSubmit).toHaveBeenCalledWith('Left button comment.');
370
+ expect(onSubmitAndMoveToAwaitingWorkspace).not.toHaveBeenCalled();
371
+ });
372
+
373
+ it('calls onSubmitAndMoveToAwaitingWorkspace and clears draft when the right button is clicked', async () => {
374
+ const onSubmit = jest.fn(stubSubmit);
375
+ const onSubmitAndMoveToAwaitingWorkspace = jest.fn(stubSubmit);
376
+ const { getByPlaceholderText, getByText } = render(
377
+ <ConsoleCommentComposer
378
+ initiallyOpen
379
+ onSubmit={onSubmit}
380
+ onSubmitAndMoveToAwaitingWorkspace={onSubmitAndMoveToAwaitingWorkspace}
381
+ />,
382
+ );
383
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
384
+ target: { value: 'Right button comment.' },
385
+ });
386
+ fireEvent.click(getByText('Comment & Awaiting Workspace'));
387
+ await waitFor(() => {
388
+ expect(getByPlaceholderText('Leave a comment…')).toHaveValue('');
389
+ });
390
+ expect(onSubmitAndMoveToAwaitingWorkspace).toHaveBeenCalledWith(
391
+ 'Right button comment.',
392
+ );
393
+ expect(onSubmit).not.toHaveBeenCalled();
394
+ });
395
+
396
+ it('shows an error message when the right button click results in an error', async () => {
397
+ const { getByPlaceholderText, getByText, findByRole } = render(
398
+ <ConsoleCommentComposer
399
+ initiallyOpen
400
+ onSubmit={stubSubmit}
401
+ onSubmitAndMoveToAwaitingWorkspace={async () => {
402
+ throw new Error('move failed');
403
+ }}
404
+ />,
405
+ );
406
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
407
+ target: { value: 'Will fail to move.' },
408
+ });
409
+ fireEvent.click(getByText('Comment & Awaiting Workspace'));
410
+ const alert = await findByRole('alert');
411
+ expect(alert.textContent).toContain('move failed');
412
+ });
413
+
332
414
  it('shows the upload failure reason when the upload rejects', async () => {
333
415
  const { getByLabelText, findByRole } = render(
334
416
  <ConsoleCommentComposer
@@ -6,6 +6,9 @@ export type ConsoleCommentComposerProps = {
6
6
  initialDraft?: string;
7
7
  onSubmit: (body: string) => Promise<ConsoleComment>;
8
8
  onDraftChange?: (draft: string) => void;
9
+ onSubmitAndMoveToAwaitingWorkspace?: (
10
+ body: string,
11
+ ) => Promise<ConsoleComment>;
9
12
  onUploadFile?: (file: File) => Promise<string>;
10
13
  };
11
14
 
@@ -78,6 +81,7 @@ export const ConsoleCommentComposer = ({
78
81
  initialDraft,
79
82
  onSubmit,
80
83
  onDraftChange,
84
+ onSubmitAndMoveToAwaitingWorkspace,
81
85
  onUploadFile,
82
86
  }: ConsoleCommentComposerProps) => {
83
87
  const [open, setOpen] = useState<boolean>(initiallyOpen);
@@ -91,14 +95,18 @@ export const ConsoleCommentComposer = ({
91
95
  });
92
96
  const fileInputRef = useRef<HTMLInputElement | null>(null);
93
97
 
94
- const submit = async (): Promise<void> => {
98
+ const submit = async (withMove: boolean): Promise<void> => {
95
99
  const body = draft.trim();
96
100
  if (body.length === 0 || status.kind === 'posting') {
97
101
  return;
98
102
  }
99
103
  setStatus({ kind: 'posting' });
100
104
  try {
101
- await onSubmit(body);
105
+ if (withMove && onSubmitAndMoveToAwaitingWorkspace !== undefined) {
106
+ await onSubmitAndMoveToAwaitingWorkspace(body);
107
+ } else {
108
+ await onSubmit(body);
109
+ }
102
110
  setDraft('');
103
111
  setStatus({ kind: 'idle' });
104
112
  } catch (error) {
@@ -231,11 +239,23 @@ export const ConsoleCommentComposer = ({
231
239
  className="console-composer-submit"
232
240
  disabled={status.kind === 'posting'}
233
241
  onClick={() => {
234
- void submit();
242
+ void submit(false);
235
243
  }}
236
244
  >
237
245
  Comment
238
246
  </button>
247
+ {onSubmitAndMoveToAwaitingWorkspace !== undefined && (
248
+ <button
249
+ type="button"
250
+ className="console-composer-submit"
251
+ disabled={status.kind === 'posting'}
252
+ onClick={() => {
253
+ void submit(true);
254
+ }}
255
+ >
256
+ Comment & Awaiting Workspace
257
+ </button>
258
+ )}
239
259
  </div>
240
260
  </div>
241
261
  )}
@@ -41,15 +41,16 @@ export type ConsoleOperationHandlers = {
41
41
  onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => void;
42
42
  };
43
43
 
44
+ export const IN_TMUX_BY_HUMAN_NAME = 'In Tmux by human';
45
+ export const AWAITING_WORKSPACE_NAME = 'Awaiting Workspace';
46
+
44
47
  export const STATUS_BUTTON_NAMES: string[] = [
45
48
  'In Tmux by agent',
46
49
  'In Tmux by human',
47
50
  'Todo by human',
48
51
  'Todo by agent',
49
- 'Awaiting Workspace',
52
+ AWAITING_WORKSPACE_NAME,
50
53
  ];
51
54
 
52
- export const IN_TMUX_BY_HUMAN_NAME = 'In Tmux by human';
53
-
54
55
  export const isManualTriageTab = (tab: ConsoleTabName): boolean =>
55
56
  tab === 'todo-by-human' || tab === 'todo-by-agent';
@@ -2,6 +2,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
2
2
  import type { ConsoleCaches } from '../hooks/useConsoleCaches';
3
3
  import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
4
4
  import { ResourceCache } from '../lib/resourceCache';
5
+ import { AWAITING_WORKSPACE_NAME } from '../logic/operations';
5
6
  import type {
6
7
  ConsoleChangedFile,
7
8
  ConsoleRelatedPullRequest,
@@ -405,6 +406,84 @@ describe('ConsoleItemDetailContainer', () => {
405
406
  ]);
406
407
  });
407
408
 
409
+ it('passes onSubmitAndMoveToAwaitingWorkspace to the composer when statusOptions includes Awaiting Workspace', () => {
410
+ const { getByText } = render(
411
+ <ConsoleItemDetailContainer
412
+ tab="todo-by-human"
413
+ item={issueItem}
414
+ caches={buildCaches()}
415
+ operations={buildOperations()}
416
+ statusOptions={consoleStatusOptionsFixture}
417
+ storyOptions={consoleStoryOptionsFixture}
418
+ storyColors={consoleStoryColorsFixture}
419
+ storyName="TDPM Console port"
420
+ overlayStatus={null}
421
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
422
+ onQueueAction={jest.fn()}
423
+ />,
424
+ );
425
+ expect(getByText('Comment & Awaiting Workspace')).toBeInTheDocument();
426
+ });
427
+
428
+ it('does not pass onSubmitAndMoveToAwaitingWorkspace to the composer when statusOptions does not include Awaiting Workspace', () => {
429
+ const statusOptionsWithoutAwaitingWorkspace =
430
+ consoleStatusOptionsFixture.filter(
431
+ (o) => o.name !== AWAITING_WORKSPACE_NAME,
432
+ );
433
+ const { queryByText } = render(
434
+ <ConsoleItemDetailContainer
435
+ tab="todo-by-human"
436
+ item={issueItem}
437
+ caches={buildCaches()}
438
+ operations={buildOperations()}
439
+ statusOptions={statusOptionsWithoutAwaitingWorkspace}
440
+ storyOptions={consoleStoryOptionsFixture}
441
+ storyColors={consoleStoryColorsFixture}
442
+ storyName="TDPM Console port"
443
+ overlayStatus={null}
444
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
445
+ onQueueAction={jest.fn()}
446
+ />,
447
+ );
448
+ expect(queryByText('Comment & Awaiting Workspace')).toBeNull();
449
+ });
450
+
451
+ it('clicking Comment & Awaiting Workspace calls addComment and queues a set_status action for the Awaiting Workspace option', async () => {
452
+ const operations = buildOperations();
453
+ const onQueueAction = jest.fn();
454
+ const { getByPlaceholderText, getByText } = render(
455
+ <ConsoleItemDetailContainer
456
+ tab="todo-by-human"
457
+ item={issueItem}
458
+ caches={buildCaches()}
459
+ operations={operations}
460
+ statusOptions={consoleStatusOptionsFixture}
461
+ storyOptions={consoleStoryOptionsFixture}
462
+ storyColors={consoleStoryColorsFixture}
463
+ storyName="TDPM Console port"
464
+ overlayStatus={null}
465
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
466
+ onQueueAction={onQueueAction}
467
+ />,
468
+ );
469
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
470
+ target: { value: 'test comment body' },
471
+ });
472
+ fireEvent.click(getByText('Comment & Awaiting Workspace'));
473
+ await waitFor(() => {
474
+ expect(operations.addComment).toHaveBeenCalledWith(
475
+ issueItem,
476
+ 'test comment body',
477
+ );
478
+ });
479
+ expect(onQueueAction).toHaveBeenCalledWith(
480
+ expect.objectContaining({
481
+ kind: { type: 'set_status', optionName: AWAITING_WORKSPACE_NAME },
482
+ item: issueItem,
483
+ }),
484
+ );
485
+ });
486
+
408
487
  it('collects an inline comment on an issue related pull request diff, enables Reject, and submits it as the request-changes review for that pull request url', async () => {
409
488
  const operations = buildOperations();
410
489
  const onQueueAction = jest.fn();
@@ -9,9 +9,10 @@ import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
9
9
  import { buildImageProxyUrl } from '../lib/imageProxy';
10
10
  import type { ConsoleActionKind } from '../logic/actionToast';
11
11
  import { resolveStoryColorEnum } from '../logic/grouping';
12
- import type {
13
- ConsoleOperationHandlers,
14
- ConsolePendingReviewComment,
12
+ import {
13
+ AWAITING_WORKSPACE_NAME,
14
+ type ConsoleOperationHandlers,
15
+ type ConsolePendingReviewComment,
15
16
  } from '../logic/operations';
16
17
  import { mergePostedComments } from '../logic/postedComments';
17
18
  import type {
@@ -163,6 +164,18 @@ export const ConsoleItemDetailContainer = ({
163
164
  },
164
165
  };
165
166
 
167
+ const awaitingWorkspaceOption =
168
+ statusOptions.find((o) => o.name === AWAITING_WORKSPACE_NAME) ?? null;
169
+
170
+ const addCommentAndMoveToAwaitingWorkspace =
171
+ awaitingWorkspaceOption !== null
172
+ ? async (body: string): Promise<ConsoleComment> => {
173
+ const comment = await addComment(body);
174
+ handlers.onSetStatus(awaitingWorkspaceOption);
175
+ return comment;
176
+ }
177
+ : undefined;
178
+
166
179
  const resolvedStoryName =
167
180
  storyName ?? (item.story.trim() !== '' ? item.story : null);
168
181
  const storyColorEnum: ConsoleColor | null =
@@ -205,6 +218,9 @@ export const ConsoleItemDetailContainer = ({
205
218
  initialDraft={initialCommentDraft}
206
219
  onSubmit={addComment}
207
220
  onDraftChange={onCommentDraftChange}
221
+ onSubmitAndMoveToAwaitingWorkspace={
222
+ addCommentAndMoveToAwaitingWorkspace
223
+ }
208
224
  onUploadFile={(file) => operations.uploadAttachment(item, file)}
209
225
  />
210
226
  }