github-issue-tower-defence-management 2.17.0 → 2.19.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 (51) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleOperationApi.js +12 -1
  3. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/assets/index-BYDbK1Fj.js +88 -0
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-DshIymWu.css → index-KyQISRuQ.css} +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  7. package/bin/adapter/entry-points/console/webServer.js +2 -0
  8. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +41 -0
  10. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +30 -0
  13. package/src/adapter/entry-points/console/consoleOperationApi.ts +14 -0
  14. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +65 -0
  15. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +12 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +8 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +81 -2
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +12 -0
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleDangerousActions.stories.tsx +14 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleDangerousActions.test.tsx +40 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleDangerousActions.tsx +37 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.stories.tsx +1 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.test.tsx +15 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.tsx +3 -4
  26. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +11 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +19 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.ts +9 -1
  29. package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +1 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +67 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +12 -0
  32. package/src/adapter/entry-points/console/ui/src/index.css +6 -0
  33. package/src/adapter/entry-points/console/ui-dist/assets/index-BYDbK1Fj.js +88 -0
  34. package/src/adapter/entry-points/console/ui-dist/assets/{index-DshIymWu.css → index-KyQISRuQ.css} +1 -1
  35. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  36. package/src/adapter/entry-points/console/webServer.ts +3 -0
  37. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +150 -0
  38. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +70 -0
  39. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
  40. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
  41. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  42. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  43. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  44. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  45. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  46. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  47. package/bin/adapter/entry-points/console/ui-dist/assets/index-DKxao68k.js +0 -88
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.stories.tsx +0 -25
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.test.tsx +0 -62
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.tsx +0 -33
  51. package/src/adapter/entry-points/console/ui-dist/assets/index-DKxao68k.js +0 -88
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, waitFor } from '@testing-library/react';
1
+ import { act, fireEvent, render, waitFor } from '@testing-library/react';
2
2
  import type { ConsoleComment } from '../../logic/types';
3
3
  import {
4
4
  appendAttachmentMarkdown,
@@ -329,14 +329,93 @@ describe('ConsoleCommentComposer', () => {
329
329
  });
330
330
  });
331
331
 
332
- it('renders only the Comment button when onSubmitAndMoveToAwaitingWorkspace is not provided', () => {
332
+ it('renders only the Comment button when neither onOkAndAwaitingWorkspace nor onSubmitAndMoveToAwaitingWorkspace is provided', () => {
333
333
  const { queryByText, getByText } = render(
334
334
  <ConsoleCommentComposer initiallyOpen onSubmit={stubSubmit} />,
335
335
  );
336
336
  expect(getByText('Comment')).toBeInTheDocument();
337
+ expect(queryByText('ok & Awaiting Workspace')).toBeNull();
337
338
  expect(queryByText('Comment & Awaiting Workspace')).toBeNull();
338
339
  });
339
340
 
341
+ it('renders ok & Awaiting Workspace between Comment and Comment & Awaiting Workspace when both callbacks are provided', () => {
342
+ const { getAllByRole } = render(
343
+ <ConsoleCommentComposer
344
+ initiallyOpen
345
+ onSubmit={stubSubmit}
346
+ onOkAndAwaitingWorkspace={() => {}}
347
+ onSubmitAndMoveToAwaitingWorkspace={stubSubmit}
348
+ />,
349
+ );
350
+ const buttons = getAllByRole('button').filter((b) =>
351
+ [
352
+ 'Comment',
353
+ 'ok & Awaiting Workspace',
354
+ 'Comment & Awaiting Workspace',
355
+ ].includes(b.textContent ?? ''),
356
+ );
357
+ expect(buttons.map((b) => b.textContent)).toEqual([
358
+ 'Comment',
359
+ 'ok & Awaiting Workspace',
360
+ 'Comment & Awaiting Workspace',
361
+ ]);
362
+ });
363
+
364
+ it('calls onOkAndAwaitingWorkspace when the ok & Awaiting Workspace button is clicked', () => {
365
+ const onOkAndAwaitingWorkspace = jest.fn();
366
+ const { getByText } = render(
367
+ <ConsoleCommentComposer
368
+ initiallyOpen
369
+ onSubmit={stubSubmit}
370
+ onOkAndAwaitingWorkspace={onOkAndAwaitingWorkspace}
371
+ />,
372
+ );
373
+ fireEvent.click(getByText('ok & Awaiting Workspace'));
374
+ expect(onOkAndAwaitingWorkspace).toHaveBeenCalledTimes(1);
375
+ });
376
+
377
+ it('disables the ok & Awaiting Workspace button while a comment is being posted', async () => {
378
+ let resolvePost!: () => void;
379
+ const slowSubmit = async (_body: string): Promise<ConsoleComment> =>
380
+ new Promise((resolve) => {
381
+ resolvePost = () =>
382
+ resolve({
383
+ author: 'HiromiShikata',
384
+ body: _body,
385
+ createdAt: '2026-06-19T11:58:00.000Z',
386
+ });
387
+ });
388
+ const { getByPlaceholderText, getByText } = render(
389
+ <ConsoleCommentComposer
390
+ initiallyOpen
391
+ onSubmit={slowSubmit}
392
+ onOkAndAwaitingWorkspace={() => {}}
393
+ onSubmitAndMoveToAwaitingWorkspace={stubSubmit}
394
+ />,
395
+ );
396
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
397
+ target: { value: 'in-flight comment' },
398
+ });
399
+ fireEvent.click(getByText('Comment'));
400
+ await waitFor(() => {
401
+ expect(getByText('ok & Awaiting Workspace')).toBeDisabled();
402
+ });
403
+ await act(async () => {
404
+ resolvePost();
405
+ });
406
+ });
407
+
408
+ it('does not render ok & Awaiting Workspace when onOkAndAwaitingWorkspace is not provided', () => {
409
+ const { queryByText } = render(
410
+ <ConsoleCommentComposer
411
+ initiallyOpen
412
+ onSubmit={stubSubmit}
413
+ onSubmitAndMoveToAwaitingWorkspace={stubSubmit}
414
+ />,
415
+ );
416
+ expect(queryByText('ok & Awaiting Workspace')).toBeNull();
417
+ });
418
+
340
419
  it('renders both Comment and Comment & Awaiting Workspace buttons when onSubmitAndMoveToAwaitingWorkspace is provided', () => {
341
420
  const { getByText } = render(
342
421
  <ConsoleCommentComposer
@@ -9,6 +9,7 @@ export type ConsoleCommentComposerProps = {
9
9
  onSubmitAndMoveToAwaitingWorkspace?: (
10
10
  body: string,
11
11
  ) => Promise<ConsoleComment>;
12
+ onOkAndAwaitingWorkspace?: () => void;
12
13
  onUploadFile?: (file: File) => Promise<string>;
13
14
  };
14
15
 
@@ -82,6 +83,7 @@ export const ConsoleCommentComposer = ({
82
83
  onSubmit,
83
84
  onDraftChange,
84
85
  onSubmitAndMoveToAwaitingWorkspace,
86
+ onOkAndAwaitingWorkspace,
85
87
  onUploadFile,
86
88
  }: ConsoleCommentComposerProps) => {
87
89
  const [open, setOpen] = useState<boolean>(initiallyOpen);
@@ -244,6 +246,16 @@ export const ConsoleCommentComposer = ({
244
246
  >
245
247
  Comment
246
248
  </button>
249
+ {onOkAndAwaitingWorkspace !== undefined && (
250
+ <button
251
+ type="button"
252
+ className="console-composer-submit"
253
+ disabled={status.kind === 'posting'}
254
+ onClick={onOkAndAwaitingWorkspace}
255
+ >
256
+ ok &amp; Awaiting Workspace
257
+ </button>
258
+ )}
247
259
  {onSubmitAndMoveToAwaitingWorkspace !== undefined && (
248
260
  <button
249
261
  type="button"
@@ -20,6 +20,7 @@ const noopOperationHandlers: ConsoleOperationHandlers = {
20
20
  onSetInTmuxByHuman: () => {},
21
21
  onClose: () => {},
22
22
  onOkAndAwaitingWorkspace: () => {},
23
+ onDeleteAllComments: () => {},
23
24
  };
24
25
 
25
26
  const richMarkdownBody = [
@@ -0,0 +1,14 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleDangerousActions } from './ConsoleDangerousActions';
3
+
4
+ const meta: Meta<typeof ConsoleDangerousActions> = {
5
+ title: 'Console/ConsoleDangerousActions',
6
+ component: ConsoleDangerousActions,
7
+ args: { onDeleteAllComments: () => {} },
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsoleDangerousActions>;
13
+
14
+ export const Default: Story = {};
@@ -0,0 +1,40 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsoleDangerousActions } from './ConsoleDangerousActions';
3
+
4
+ describe('ConsoleDangerousActions', () => {
5
+ it('renders only the toggle button in the collapsed state', () => {
6
+ const { getByText, queryByText } = render(
7
+ <ConsoleDangerousActions onDeleteAllComments={() => {}} />,
8
+ );
9
+ expect(getByText('⚠')).toBeInTheDocument();
10
+ expect(queryByText('Delete All Comments')).toBeNull();
11
+ });
12
+
13
+ it('reveals the Delete All Comments button after the toggle is clicked', () => {
14
+ const { getByText } = render(
15
+ <ConsoleDangerousActions onDeleteAllComments={() => {}} />,
16
+ );
17
+ fireEvent.click(getByText('⚠'));
18
+ expect(getByText('Delete All Comments')).toBeInTheDocument();
19
+ });
20
+
21
+ it('collapses back when the toggle is clicked a second time', () => {
22
+ const { getByText, queryByText } = render(
23
+ <ConsoleDangerousActions onDeleteAllComments={() => {}} />,
24
+ );
25
+ fireEvent.click(getByText('⚠'));
26
+ fireEvent.click(getByText('⚠'));
27
+ expect(queryByText('Delete All Comments')).toBeNull();
28
+ });
29
+
30
+ it('calls onDeleteAllComments and collapses when Delete All Comments is clicked', () => {
31
+ const onDeleteAllComments = jest.fn();
32
+ const { getByText, queryByText } = render(
33
+ <ConsoleDangerousActions onDeleteAllComments={onDeleteAllComments} />,
34
+ );
35
+ fireEvent.click(getByText('⚠'));
36
+ fireEvent.click(getByText('Delete All Comments'));
37
+ expect(onDeleteAllComments).toHaveBeenCalledTimes(1);
38
+ expect(queryByText('Delete All Comments')).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1,37 @@
1
+ import { useState } from 'react';
2
+
3
+ export type ConsoleDangerousActionsProps = {
4
+ onDeleteAllComments: () => void;
5
+ };
6
+
7
+ export const ConsoleDangerousActions = ({
8
+ onDeleteAllComments,
9
+ }: ConsoleDangerousActionsProps) => {
10
+ const [expanded, setExpanded] = useState(false);
11
+
12
+ const handleDeleteAllComments = () => {
13
+ onDeleteAllComments();
14
+ setExpanded(false);
15
+ };
16
+
17
+ return (
18
+ <div className="console-op-group">
19
+ <button
20
+ type="button"
21
+ className="console-op-button console-op-button-danger"
22
+ onClick={() => setExpanded((prev) => !prev)}
23
+ >
24
+ ⚠
25
+ </button>
26
+ {expanded && (
27
+ <button
28
+ type="button"
29
+ className="console-op-button console-op-button-danger"
30
+ onClick={handleDeleteAllComments}
31
+ >
32
+ Delete All Comments
33
+ </button>
34
+ )}
35
+ </div>
36
+ );
37
+ };
@@ -14,6 +14,7 @@ const handlers: ConsoleOperationHandlers = {
14
14
  onSetInTmuxByHuman: () => {},
15
15
  onClose: () => {},
16
16
  onOkAndAwaitingWorkspace: () => {},
17
+ onDeleteAllComments: () => {},
17
18
  };
18
19
 
19
20
  const meta: Meta<typeof ConsoleOperationMenu> = {
@@ -14,6 +14,7 @@ const handlers: ConsoleOperationHandlers = {
14
14
  onSetInTmuxByHuman: jest.fn(),
15
15
  onClose: jest.fn(),
16
16
  onOkAndAwaitingWorkspace: jest.fn(),
17
+ onDeleteAllComments: jest.fn(),
17
18
  };
18
19
 
19
20
  const prItem = consoleListItemsFixture[0];
@@ -141,4 +142,18 @@ describe('ConsoleOperationMenu', () => {
141
142
  );
142
143
  expect(getByText('Reject')).not.toBeDisabled();
143
144
  });
145
+
146
+ it('renders the dangerous actions toggle on all tabs', () => {
147
+ const { getByText } = render(
148
+ <ConsoleOperationMenu
149
+ tab="prs"
150
+ item={prItem}
151
+ hasPullRequest
152
+ rejectEnabled={false}
153
+ statusOptions={consoleStatusOptionsFixture}
154
+ handlers={handlers}
155
+ />,
156
+ );
157
+ expect(getByText('⚠')).toBeInTheDocument();
158
+ });
144
159
  });
@@ -8,8 +8,8 @@ import type {
8
8
  ConsoleTabName,
9
9
  } from '../../logic/types';
10
10
  import { ConsoleCloseActions } from './ConsoleCloseActions';
11
+ import { ConsoleDangerousActions } from './ConsoleDangerousActions';
11
12
  import { ConsoleNextActionDateActions } from './ConsoleNextActionDateActions';
12
- import { ConsoleOkAndAwaitingWorkspaceActions } from './ConsoleOkAndAwaitingWorkspaceActions';
13
13
  import { ConsolePullRequestReviewActions } from './ConsolePullRequestReviewActions';
14
14
  import { ConsoleStatusActions } from './ConsoleStatusActions';
15
15
 
@@ -31,9 +31,8 @@ export const ConsoleOperationMenu = ({
31
31
  }: ConsoleOperationBarProps) => {
32
32
  return (
33
33
  <div className="console-operation-bar">
34
- <ConsoleOkAndAwaitingWorkspaceActions
35
- statusOptions={statusOptions}
36
- onOkAndAwaitingWorkspace={handlers.onOkAndAwaitingWorkspace}
34
+ <ConsoleDangerousActions
35
+ onDeleteAllComments={handlers.onDeleteAllComments}
37
36
  />
38
37
  {hasPullRequest && (
39
38
  <ConsolePullRequestReviewActions
@@ -7,6 +7,7 @@ import {
7
7
  encodeAttachmentContent,
8
8
  postConsoleAttachment,
9
9
  postConsoleComment,
10
+ postConsoleDeleteAllComments,
10
11
  postConsoleOperation,
11
12
  postConsoleReviewComment,
12
13
  } from '../lib/consoleApi';
@@ -74,6 +75,7 @@ export type ConsoleOperationsApi = {
74
75
  side: ConsoleReviewCommentSide,
75
76
  body: string,
76
77
  ) => Promise<void>;
78
+ deleteAllComments: (item: ConsoleListItem) => Promise<void>;
77
79
  };
78
80
 
79
81
  export const reviewRequest = (
@@ -391,6 +393,14 @@ export const useConsoleOperations = (
391
393
  [pjcode],
392
394
  );
393
395
 
396
+ const deleteAllComments = useCallback(
397
+ async (item: ConsoleListItem) => {
398
+ await postConsoleDeleteAllComments({ issueUrl: item.url });
399
+ invalidateItemContent(item);
400
+ },
401
+ [invalidateItemContent],
402
+ );
403
+
394
404
  return {
395
405
  reviewPullRequest,
396
406
  setNextActionDate,
@@ -402,5 +412,6 @@ export const useConsoleOperations = (
402
412
  addComment,
403
413
  uploadAttachment,
404
414
  addInlineReviewComment,
415
+ deleteAllComments,
405
416
  };
406
417
  };
@@ -482,6 +482,25 @@ export const postConsoleReorderStory = async (
482
482
  }
483
483
  };
484
484
 
485
+ export const DELETE_ALL_COMMENTS_OPERATION_PATH = '/api/deleteallcomments';
486
+
487
+ export type ConsoleDeleteAllCommentsRequest = {
488
+ issueUrl: string;
489
+ };
490
+
491
+ export const postConsoleDeleteAllComments = async (
492
+ request: ConsoleDeleteAllCommentsRequest,
493
+ ): Promise<void> => {
494
+ const response = await fetch(DELETE_ALL_COMMENTS_OPERATION_PATH, {
495
+ method: 'POST',
496
+ headers: { 'Content-Type': 'application/json' },
497
+ body: JSON.stringify(request),
498
+ });
499
+ if (!response.ok) {
500
+ throw new Error(await readOperationErrorReason(response));
501
+ }
502
+ };
503
+
485
504
  export type ProjectListResponse = {
486
505
  pjcodes: string[];
487
506
  workflowImprovementIssueUrl: string | null;
@@ -21,7 +21,8 @@ export type ConsoleActionKind =
21
21
  | { type: 'set_status'; optionName: string }
22
22
  | { type: 'set_in_tmux_by_human'; optionName: string }
23
23
  | { type: 'close'; action: ConsoleCloseAction }
24
- | { type: 'ok_and_awaiting_workspace' };
24
+ | { type: 'ok_and_awaiting_workspace' }
25
+ | { type: 'delete_all_comments' };
25
26
 
26
27
  export const ACTION_TOAST_DELAY_MS = 5000;
27
28
 
@@ -67,6 +68,8 @@ export const actionToastMessage = (
67
68
  return 'ok → Awaiting Workspace';
68
69
  case 'close':
69
70
  return kind.action === 'close' ? 'Closed' : 'Closed as not planned';
71
+ case 'delete_all_comments':
72
+ return 'Deleted all comments';
70
73
  }
71
74
  };
72
75
 
@@ -94,6 +97,8 @@ export const actionToastColor = (
94
97
  return 'blue';
95
98
  case 'close':
96
99
  return 'red';
100
+ case 'delete_all_comments':
101
+ return 'red';
97
102
  }
98
103
  };
99
104
 
@@ -104,6 +109,9 @@ export const actionAdvances = (
104
109
  if (kind.type === 'next_action_date') {
105
110
  return isManualTriageTab(tab);
106
111
  }
112
+ if (kind.type === 'delete_all_comments') {
113
+ return false;
114
+ }
107
115
  return true;
108
116
  };
109
117
 
@@ -43,6 +43,7 @@ export type ConsoleOperationHandlers = {
43
43
  onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
44
44
  onClose: (action: ConsoleCloseAction) => void;
45
45
  onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => void;
46
+ onDeleteAllComments: () => void;
46
47
  };
47
48
 
48
49
  export const IN_TMUX_BY_HUMAN_NAME = 'In Tmux by human';
@@ -82,6 +82,7 @@ const buildOperations = (): ConsoleOperationsApi => ({
82
82
  })),
83
83
  uploadAttachment: jest.fn(async () => ''),
84
84
  addInlineReviewComment: jest.fn(async () => {}),
85
+ deleteAllComments: jest.fn(async () => {}),
85
86
  });
86
87
 
87
88
  const findCommentsPanelToggle = (container: HTMLElement): HTMLElement => {
@@ -559,4 +560,70 @@ describe('ConsoleItemDetailContainer', () => {
559
560
  ]);
560
561
  expect(reviewCall?.[3][0].body).not.toBe('');
561
562
  });
563
+
564
+ it('routes delete-all-comments through onQueueAction with the correct kind and commit', async () => {
565
+ const operations = buildOperations();
566
+ const onQueueAction = jest.fn();
567
+ const { getByText } = render(
568
+ <ConsoleItemDetailContainer
569
+ tab="todo-by-agent"
570
+ item={issueItem}
571
+ caches={buildCaches()}
572
+ operations={operations}
573
+ statusOptions={consoleStatusOptionsFixture}
574
+ storyColors={consoleStoryColorsFixture}
575
+ storyName="TDPM Console port"
576
+ overlayStatus={null}
577
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
578
+ onQueueAction={onQueueAction}
579
+ />,
580
+ );
581
+
582
+ await waitFor(() => {
583
+ expect(getByText('⚠')).toBeInTheDocument();
584
+ });
585
+
586
+ fireEvent.click(getByText('⚠'));
587
+ fireEvent.click(getByText('Delete All Comments'));
588
+
589
+ expect(onQueueAction).toHaveBeenCalledTimes(1);
590
+ const input = onQueueAction.mock.calls[0][0];
591
+ expect(input.kind).toEqual({ type: 'delete_all_comments' });
592
+ expect(input.item).toBe(issueItem);
593
+ expect(operations.deleteAllComments).not.toHaveBeenCalled();
594
+
595
+ await input.commit();
596
+ expect(operations.deleteAllComments).toHaveBeenCalledWith(issueItem);
597
+ });
598
+
599
+ it('surfaces errors from deleteAllComments via the queue rather than swallowing them', async () => {
600
+ const operations = buildOperations();
601
+ const error = new Error('GitHub API failure');
602
+ (operations.deleteAllComments as jest.Mock).mockRejectedValue(error);
603
+ const onQueueAction = jest.fn();
604
+ const { getByText } = render(
605
+ <ConsoleItemDetailContainer
606
+ tab="todo-by-agent"
607
+ item={issueItem}
608
+ caches={buildCaches()}
609
+ operations={operations}
610
+ statusOptions={consoleStatusOptionsFixture}
611
+ storyColors={consoleStoryColorsFixture}
612
+ storyName="TDPM Console port"
613
+ overlayStatus={null}
614
+ now={Date.parse('2026-06-19T12:00:00.000Z')}
615
+ onQueueAction={onQueueAction}
616
+ />,
617
+ );
618
+
619
+ await waitFor(() => {
620
+ expect(getByText('⚠')).toBeInTheDocument();
621
+ });
622
+
623
+ fireEvent.click(getByText('⚠'));
624
+ fireEvent.click(getByText('Delete All Comments'));
625
+
626
+ const input = onQueueAction.mock.calls[0][0];
627
+ await expect(input.commit()).rejects.toThrow('GitHub API failure');
628
+ });
562
629
  });
@@ -262,6 +262,13 @@ export const ConsoleItemDetailContainer = ({
262
262
  commit: () => operations.okAndMoveToAwaitingWorkspace(item, option),
263
263
  });
264
264
  },
265
+ onDeleteAllComments: () => {
266
+ onQueueAction({
267
+ kind: { type: 'delete_all_comments' },
268
+ item,
269
+ commit: () => operations.deleteAllComments(item),
270
+ });
271
+ },
265
272
  };
266
273
 
267
274
  const awaitingWorkspaceOption =
@@ -318,6 +325,11 @@ export const ConsoleItemDetailContainer = ({
318
325
  initialDraft={initialCommentDraft}
319
326
  onSubmit={addComment}
320
327
  onDraftChange={onCommentDraftChange}
328
+ onOkAndAwaitingWorkspace={
329
+ awaitingWorkspaceOption !== null
330
+ ? () => handlers.onOkAndAwaitingWorkspace(awaitingWorkspaceOption)
331
+ : undefined
332
+ }
321
333
  onSubmitAndMoveToAwaitingWorkspace={
322
334
  addCommentAndMoveToAwaitingWorkspace
323
335
  }
@@ -1133,6 +1133,12 @@ body {
1133
1133
  color: #79c0ff;
1134
1134
  }
1135
1135
 
1136
+ .console-op-button-danger {
1137
+ background: #3a1518;
1138
+ border-color: #f85149;
1139
+ color: #f85149;
1140
+ }
1141
+
1136
1142
  .console-pr-section {
1137
1143
  border: 1px solid #30363d;
1138
1144
  border-radius: 8px;