github-issue-tower-defence-management 1.180.0 → 1.181.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 (73) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/adapter/entry-points/console/ui-dist/assets/index-BUFcVUVD.js +86 -0
  3. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  4. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +41 -12
  5. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -1
  6. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +2 -1
  7. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  8. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +2 -1
  9. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  10. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +2 -1
  11. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  12. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +3 -3
  13. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
  14. package/bin/domain/usecases/extractFencedJsonBlocks.js +18 -0
  15. package/bin/domain/usecases/extractFencedJsonBlocks.js.map +1 -0
  16. package/bin/domain/usecases/extractNextStepAgent.js +14 -23
  17. package/bin/domain/usecases/extractNextStepAgent.js.map +1 -1
  18. package/bin/domain/usecases/findLastAgentReport.js +2 -3
  19. package/bin/domain/usecases/findLastAgentReport.js.map +1 -1
  20. package/bin/domain/usecases/isAgentReportBody.js +43 -0
  21. package/bin/domain/usecases/isAgentReportBody.js.map +1 -0
  22. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js +12 -19
  23. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +1 -1
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.stories.tsx +25 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.test.tsx +50 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.tsx +41 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.stories.tsx +1 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.test.tsx +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.tsx +5 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +70 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +29 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.test.ts +14 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.ts +5 -1
  37. package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +1 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +7 -0
  40. package/src/adapter/entry-points/console/ui-dist/assets/index-BUFcVUVD.js +86 -0
  41. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  42. package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +47 -0
  43. package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +76 -36
  44. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -1
  45. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +50 -0
  46. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +2 -1
  47. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +2 -1
  48. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +6 -5
  49. package/src/domain/usecases/extractFencedJsonBlocks.test.ts +40 -0
  50. package/src/domain/usecases/extractFencedJsonBlocks.ts +21 -0
  51. package/src/domain/usecases/extractNextStepAgent.test.ts +19 -0
  52. package/src/domain/usecases/extractNextStepAgent.ts +15 -27
  53. package/src/domain/usecases/findLastAgentReport.test.ts +28 -0
  54. package/src/domain/usecases/findLastAgentReport.ts +2 -3
  55. package/src/domain/usecases/isAgentReportBody.test.ts +108 -0
  56. package/src/domain/usecases/isAgentReportBody.ts +52 -0
  57. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +33 -0
  58. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +15 -23
  59. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +3 -1
  60. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -1
  61. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  62. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  63. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  64. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
  65. package/types/domain/usecases/extractFencedJsonBlocks.d.ts +2 -0
  66. package/types/domain/usecases/extractFencedJsonBlocks.d.ts.map +1 -0
  67. package/types/domain/usecases/extractNextStepAgent.d.ts.map +1 -1
  68. package/types/domain/usecases/findLastAgentReport.d.ts.map +1 -1
  69. package/types/domain/usecases/isAgentReportBody.d.ts +4 -0
  70. package/types/domain/usecases/isAgentReportBody.d.ts.map +1 -0
  71. package/types/domain/usecases/isPullRequestDeclaredUnnecessary.d.ts.map +1 -1
  72. package/bin/adapter/entry-points/console/ui-dist/assets/index-I_2Yu8fM.js +0 -86
  73. package/src/adapter/entry-points/console/ui-dist/assets/index-I_2Yu8fM.js +0 -86
@@ -1,32 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isPullRequestDeclaredUnnecessary = void 0;
4
- const agentReportPrefix_1 = require("./agentReportPrefix");
5
- const normalizeReportBody_1 = require("./normalizeReportBody");
4
+ const extractFencedJsonBlocks_1 = require("./extractFencedJsonBlocks");
5
+ const isAgentReportBody_1 = require("./isAgentReportBody");
6
6
  const isPullRequestDeclaredUnnecessary = (comments, isTrustedAuthor) => {
7
7
  const lastComment = comments[comments.length - 1];
8
8
  if (!lastComment ||
9
9
  !isTrustedAuthor(lastComment.author) ||
10
- !lastComment.content.startsWith(agentReportPrefix_1.AGENT_REPORT_PREFIX)) {
10
+ !(0, isAgentReportBody_1.isAgentReportBody)(lastComment.content)) {
11
11
  return false;
12
12
  }
13
- const reportMatch = (0, normalizeReportBody_1.normalizeReportBody)(lastComment.content).match(/```json\n([\s\S]*?)\n```/);
14
- if (!reportMatch || reportMatch.length < 2) {
15
- return false;
16
- }
17
- let reportJson;
18
- try {
19
- reportJson = JSON.parse(reportMatch[1]);
20
- }
21
- catch (error) {
22
- console.warn('Invalid JSON in report body while checking pullRequestRequired:', error);
23
- return false;
24
- }
25
- if (typeof reportJson !== 'object' || reportJson === null) {
26
- return false;
13
+ for (const block of (0, extractFencedJsonBlocks_1.extractFencedJsonBlocks)(lastComment.content, 'pullRequestRequired')) {
14
+ if (typeof block !== 'object' || block === null) {
15
+ continue;
16
+ }
17
+ const report = { ...block };
18
+ if (report.pullRequestRequired === false) {
19
+ return true;
20
+ }
27
21
  }
28
- const report = { ...reportJson };
29
- return report.pullRequestRequired === false;
22
+ return false;
30
23
  };
31
24
  exports.isPullRequestDeclaredUnnecessary = isPullRequestDeclaredUnnecessary;
32
25
  //# sourceMappingURL=isPullRequestDeclaredUnnecessary.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"isPullRequestDeclaredUnnecessary.js","sourceRoot":"","sources":["../../../src/domain/usecases/isPullRequestDeclaredUnnecessary.ts"],"names":[],"mappings":";;;AAAA,2DAA0D;AAC1D,+DAA4D;AAErD,MAAM,gCAAgC,GAAG,CAC9C,QAA+C,EAC/C,eAA4C,EACnC,EAAE;IACX,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAClD,IACE,CAAC,WAAW;QACZ,CAAC,eAAe,CAAC,WAAW,CAAC,MAAM,CAAC;QACpC,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,uCAAmB,CAAC,EACpD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,WAAW,GAAG,IAAA,yCAAmB,EAAC,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,CAChE,0BAA0B,CAC3B,CAAC;IACF,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,UAAmB,CAAC;IACxB,IAAI,CAAC;QACH,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CACV,iEAAiE,EACjE,KAAK,CACN,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAA4B,EAAE,GAAG,UAAU,EAAE,CAAC;IAC1D,OAAO,MAAM,CAAC,mBAAmB,KAAK,KAAK,CAAC;AAC9C,CAAC,CAAC;AAjCW,QAAA,gCAAgC,oCAiC3C"}
1
+ {"version":3,"file":"isPullRequestDeclaredUnnecessary.js","sourceRoot":"","sources":["../../../src/domain/usecases/isPullRequestDeclaredUnnecessary.ts"],"names":[],"mappings":";;;AAAA,uEAAoE;AACpE,2DAAwD;AAEjD,MAAM,gCAAgC,GAAG,CAC9C,QAA+C,EAC/C,eAA4C,EACnC,EAAE;IACX,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAClD,IACE,CAAC,WAAW;QACZ,CAAC,eAAe,CAAC,WAAW,CAAC,MAAM,CAAC;QACpC,CAAC,IAAA,qCAAiB,EAAC,WAAW,CAAC,OAAO,CAAC,EACvC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAA,iDAAuB,EACzC,WAAW,CAAC,OAAO,EACnB,qBAAqB,CACtB,EAAE,CAAC;QACF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAChD,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAA4B,EAAE,GAAG,KAAK,EAAE,CAAC;QACrD,IAAI,MAAM,CAAC,mBAAmB,KAAK,KAAK,EAAE,CAAC;YACzC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAzBW,QAAA,gCAAgC,oCAyB3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.180.0",
3
+ "version": "1.181.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -112,7 +112,7 @@ test('renders the Workflow Blocker tab leftmost and shows its detail operations'
112
112
  ).click();
113
113
 
114
114
  await expect(
115
- page.locator('.console-op-button', { hasText: 'Awaiting Workspace' }),
115
+ page.locator('.console-op-button', { hasText: /^Awaiting Workspace$/ }),
116
116
  ).toBeVisible();
117
117
  await expect(
118
118
  page.locator('.console-op-button', { hasText: 'Close as not planned' }),
@@ -20,6 +20,7 @@ const noopOperationHandlers: ConsoleOperationHandlers = {
20
20
  onSetStatus: () => {},
21
21
  onSetInTmuxByHuman: () => {},
22
22
  onClose: () => {},
23
+ onOkAndAwaitingWorkspace: () => {},
23
24
  };
24
25
 
25
26
  const richMarkdownBody = [
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleStatusOptionsFixture } from '../../testing/fixtures';
3
+ import { ConsoleOkAndAwaitingWorkspaceActions } from './ConsoleOkAndAwaitingWorkspaceActions';
4
+
5
+ const meta: Meta<typeof ConsoleOkAndAwaitingWorkspaceActions> = {
6
+ title: 'Console/ConsoleOkAndAwaitingWorkspaceActions',
7
+ component: ConsoleOkAndAwaitingWorkspaceActions,
8
+ args: { onOkAndAwaitingWorkspace: () => {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof ConsoleOkAndAwaitingWorkspaceActions>;
14
+
15
+ export const WithAwaitingWorkspaceOption: Story = {
16
+ args: { statusOptions: consoleStatusOptionsFixture },
17
+ };
18
+
19
+ export const WithoutAwaitingWorkspaceOption: Story = {
20
+ args: {
21
+ statusOptions: consoleStatusOptionsFixture.filter(
22
+ (o) => o.name.toLowerCase() !== 'awaiting workspace',
23
+ ),
24
+ },
25
+ };
@@ -0,0 +1,50 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { consoleStatusOptionsFixture } from '../../testing/fixtures';
3
+ import { ConsoleOkAndAwaitingWorkspaceActions } from './ConsoleOkAndAwaitingWorkspaceActions';
4
+
5
+ const [awaitingWorkspaceOption] = consoleStatusOptionsFixture.filter(
6
+ (o) => o.name.toLowerCase() === 'awaiting workspace',
7
+ );
8
+
9
+ const statusOptionsWithoutAwaitingWorkspace =
10
+ consoleStatusOptionsFixture.filter(
11
+ (o) => o.name.toLowerCase() !== 'awaiting workspace',
12
+ );
13
+
14
+ describe('ConsoleOkAndAwaitingWorkspaceActions', () => {
15
+ it('renders the button when the awaiting workspace option is present', () => {
16
+ const { getByRole } = render(
17
+ <ConsoleOkAndAwaitingWorkspaceActions
18
+ statusOptions={consoleStatusOptionsFixture}
19
+ onOkAndAwaitingWorkspace={() => {}}
20
+ />,
21
+ );
22
+ expect(
23
+ getByRole('button', { name: 'ok & Awaiting Workspace' }),
24
+ ).toBeInTheDocument();
25
+ });
26
+
27
+ it('returns null when the awaiting workspace option is absent', () => {
28
+ const { container } = render(
29
+ <ConsoleOkAndAwaitingWorkspaceActions
30
+ statusOptions={statusOptionsWithoutAwaitingWorkspace}
31
+ onOkAndAwaitingWorkspace={() => {}}
32
+ />,
33
+ );
34
+ expect(container.firstChild).toBeNull();
35
+ });
36
+
37
+ it('calls the callback with the awaiting workspace option on click', () => {
38
+ const onOkAndAwaitingWorkspace = jest.fn();
39
+ const { getByRole } = render(
40
+ <ConsoleOkAndAwaitingWorkspaceActions
41
+ statusOptions={consoleStatusOptionsFixture}
42
+ onOkAndAwaitingWorkspace={onOkAndAwaitingWorkspace}
43
+ />,
44
+ );
45
+ fireEvent.click(getByRole('button', { name: 'ok & Awaiting Workspace' }));
46
+ expect(onOkAndAwaitingWorkspace).toHaveBeenCalledWith(
47
+ awaitingWorkspaceOption,
48
+ );
49
+ });
50
+ });
@@ -0,0 +1,41 @@
1
+ import { colorFromEnum } from '../../logic/colors';
2
+ import type { ConsoleFieldOption } from '../../logic/types';
3
+
4
+ export type ConsoleOkAndAwaitingWorkspaceActionsProps = {
5
+ statusOptions: ConsoleFieldOption[];
6
+ onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => void;
7
+ };
8
+
9
+ const AWAITING_WORKSPACE_NAME = 'awaiting workspace';
10
+
11
+ export const ConsoleOkAndAwaitingWorkspaceActions = ({
12
+ statusOptions,
13
+ onOkAndAwaitingWorkspace,
14
+ }: ConsoleOkAndAwaitingWorkspaceActionsProps) => {
15
+ const option = statusOptions.find(
16
+ (o) => o.name.toLowerCase() === AWAITING_WORKSPACE_NAME,
17
+ );
18
+
19
+ if (option === undefined) {
20
+ return null;
21
+ }
22
+
23
+ const palette = colorFromEnum(option.color);
24
+
25
+ return (
26
+ <div className="console-op-group">
27
+ <button
28
+ type="button"
29
+ className="console-op-button"
30
+ style={{
31
+ color: palette.fg,
32
+ borderColor: palette.border,
33
+ backgroundColor: palette.bg,
34
+ }}
35
+ onClick={() => onOkAndAwaitingWorkspace(option)}
36
+ >
37
+ ok &amp; Awaiting Workspace
38
+ </button>
39
+ </div>
40
+ );
41
+ };
@@ -14,6 +14,7 @@ const handlers: ConsoleOperationHandlers = {
14
14
  onSetStatus: () => {},
15
15
  onSetInTmuxByHuman: () => {},
16
16
  onClose: () => {},
17
+ onOkAndAwaitingWorkspace: () => {},
17
18
  };
18
19
 
19
20
  const meta: Meta<typeof ConsoleOperationMenu> = {
@@ -14,6 +14,7 @@ const handlers: ConsoleOperationHandlers = {
14
14
  onSetStatus: jest.fn(),
15
15
  onSetInTmuxByHuman: jest.fn(),
16
16
  onClose: jest.fn(),
17
+ onOkAndAwaitingWorkspace: jest.fn(),
17
18
  };
18
19
 
19
20
  const prItem = consoleListItemsFixture[0];
@@ -9,6 +9,7 @@ import type {
9
9
  } from '../../logic/types';
10
10
  import { ConsoleCloseActions } from './ConsoleCloseActions';
11
11
  import { ConsoleNextActionDateActions } from './ConsoleNextActionDateActions';
12
+ import { ConsoleOkAndAwaitingWorkspaceActions } from './ConsoleOkAndAwaitingWorkspaceActions';
12
13
  import { ConsolePullRequestReviewActions } from './ConsolePullRequestReviewActions';
13
14
  import { ConsoleStatusActions } from './ConsoleStatusActions';
14
15
  import { ConsoleStoryActions } from './ConsoleStoryActions';
@@ -34,6 +35,10 @@ export const ConsoleOperationMenu = ({
34
35
  const showStory = tab === 'triage';
35
36
  return (
36
37
  <div className="console-operation-bar">
38
+ <ConsoleOkAndAwaitingWorkspaceActions
39
+ statusOptions={statusOptions}
40
+ onOkAndAwaitingWorkspace={handlers.onOkAndAwaitingWorkspace}
41
+ />
37
42
  {hasPullRequest && (
38
43
  <ConsolePullRequestReviewActions
39
44
  onReview={handlers.onReview}
@@ -415,4 +415,74 @@ describe('useConsoleOperations', () => {
415
415
  ).rejects.toThrow('No project specified in the URL path.');
416
416
  expect(fetchMock).not.toHaveBeenCalled();
417
417
  });
418
+
419
+ it('posts an ok comment then sets status to awaiting workspace in order', async () => {
420
+ let callCount = 0;
421
+ const calls: Array<[string, unknown]> = [];
422
+ global.fetch = jest.fn(async (url: unknown, opts: unknown) => {
423
+ calls.push([url as string, opts]);
424
+ callCount += 1;
425
+ if (callCount === 1) {
426
+ return {
427
+ ok: true,
428
+ status: 200,
429
+ json: async () => ({
430
+ comment: {
431
+ author: 'bot',
432
+ body: 'ok',
433
+ createdAt: '2026-01-01T00:00:00Z',
434
+ },
435
+ }),
436
+ };
437
+ }
438
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
439
+ }) as unknown as typeof fetch;
440
+ const { result } = setup();
441
+ const [option] = consoleStatusOptionsFixture.filter(
442
+ (o) => o.name.toLowerCase() === 'awaiting workspace',
443
+ );
444
+ await act(async () => {
445
+ await result.current.operations.okAndMoveToAwaitingWorkspace(
446
+ issueItem,
447
+ option,
448
+ );
449
+ });
450
+ expect(calls[0][0]).toBe('/api/comment');
451
+ expect(JSON.parse((calls[0][1] as { body: string }).body)).toMatchObject({
452
+ pjcode: 'acme',
453
+ url: issueItem.url,
454
+ body: 'ok',
455
+ });
456
+ expect(calls[1][0]).toBe('/api/triage');
457
+ expect(JSON.parse((calls[1][1] as { body: string }).body)).toMatchObject({
458
+ pjcode: 'acme',
459
+ action: 'set_status',
460
+ issueUrl: issueItem.url,
461
+ statusName: option.name,
462
+ });
463
+ });
464
+
465
+ it('does not call the status endpoint when the comment post fails', async () => {
466
+ let callCount = 0;
467
+ global.fetch = jest.fn(async () => {
468
+ callCount += 1;
469
+ if (callCount === 1) {
470
+ return { ok: false, status: 500, text: async () => 'error' };
471
+ }
472
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
473
+ }) as unknown as typeof fetch;
474
+ const { result } = setup();
475
+ const [option] = consoleStatusOptionsFixture.filter(
476
+ (o) => o.name.toLowerCase() === 'awaiting workspace',
477
+ );
478
+ await expect(
479
+ act(async () => {
480
+ await result.current.operations.okAndMoveToAwaitingWorkspace(
481
+ issueItem,
482
+ option,
483
+ );
484
+ }),
485
+ ).rejects.toThrow();
486
+ expect(callCount).toBe(1);
487
+ });
418
488
  });
@@ -61,6 +61,10 @@ export type ConsoleOperationsApi = {
61
61
  item: ConsoleListItem,
62
62
  action: ConsoleCloseAction,
63
63
  ) => Promise<void>;
64
+ okAndMoveToAwaitingWorkspace: (
65
+ item: ConsoleListItem,
66
+ option: ConsoleFieldOption,
67
+ ) => Promise<void>;
64
68
  addComment: (item: ConsoleListItem, body: string) => Promise<ConsoleComment>;
65
69
  uploadAttachment: (item: ConsoleListItem, file: File) => Promise<string>;
66
70
  addInlineReviewComment: (
@@ -276,6 +280,30 @@ export const useConsoleOperations = (
276
280
  [pjcode, markDone],
277
281
  );
278
282
 
283
+ const okAndMoveToAwaitingWorkspace = useCallback(
284
+ async (item: ConsoleListItem, option: ConsoleFieldOption) => {
285
+ if (pjcode === null) {
286
+ throw missingPjcodeError();
287
+ }
288
+ await postConsoleComment({ pjcode, url: item.url, body: 'ok' });
289
+ const request: ConsoleTriageRequest = {
290
+ pjcode,
291
+ action: 'set_status',
292
+ issueUrl: item.url,
293
+ projectItemId: item.projectItemId,
294
+ statusName: option.name,
295
+ };
296
+ await postConsoleOperation(TRIAGE_OPERATION_PATH, request);
297
+ invalidateItemContent(item);
298
+ patchOverlay(
299
+ overlayKeyForItem(item),
300
+ { done: true, status: { name: option.name, color: option.color } },
301
+ mode,
302
+ );
303
+ },
304
+ [pjcode, invalidateItemContent, patchOverlay, mode],
305
+ );
306
+
279
307
  const addComment = useCallback(
280
308
  async (item: ConsoleListItem, body: string) => {
281
309
  if (pjcode === null) {
@@ -340,6 +368,7 @@ export const useConsoleOperations = (
340
368
  setStatus,
341
369
  setInTmuxByHuman,
342
370
  closeIssue,
371
+ okAndMoveToAwaitingWorkspace,
343
372
  addComment,
344
373
  uploadAttachment,
345
374
  addInlineReviewComment,
@@ -127,6 +127,17 @@ describe('actionToastColor', () => {
127
127
  actionToastColor({ type: 'next_action_date', action: 'snooze_1day' }),
128
128
  ).toBe('amber');
129
129
  expect(actionToastColor({ type: 'close', action: 'close' })).toBe('red');
130
+ expect(actionToastColor({ type: 'ok_and_awaiting_workspace' })).toBe(
131
+ 'blue',
132
+ );
133
+ });
134
+ });
135
+
136
+ describe('actionToastMessage for ok_and_awaiting_workspace', () => {
137
+ it('returns the composite label', () => {
138
+ expect(
139
+ actionToastMessage({ type: 'ok_and_awaiting_workspace' }, 'prs'),
140
+ ).toBe('ok → Awaiting Workspace');
130
141
  });
131
142
  });
132
143
 
@@ -138,6 +149,9 @@ describe('actionAdvances', () => {
138
149
  expect(actionAdvances({ type: 'set_status', optionName: 'x' }, 'prs')).toBe(
139
150
  true,
140
151
  );
152
+ expect(actionAdvances({ type: 'ok_and_awaiting_workspace' }, 'prs')).toBe(
153
+ true,
154
+ );
141
155
  });
142
156
 
143
157
  it('does not advance a snooze except on the manual triage tabs', () => {
@@ -20,7 +20,8 @@ export type ConsoleActionKind =
20
20
  | { type: 'set_story'; optionName: string }
21
21
  | { type: 'set_status'; optionName: string }
22
22
  | { type: 'set_in_tmux_by_human'; optionName: string }
23
- | { type: 'close'; action: ConsoleCloseAction };
23
+ | { type: 'close'; action: ConsoleCloseAction }
24
+ | { type: 'ok_and_awaiting_workspace' };
24
25
 
25
26
  export const ACTION_TOAST_DELAY_MS = 5000;
26
27
 
@@ -56,6 +57,8 @@ export const actionToastMessage = (
56
57
  return `Status → ${kind.optionName}`;
57
58
  case 'set_in_tmux_by_human':
58
59
  return 'Added to In Tmux by human';
60
+ case 'ok_and_awaiting_workspace':
61
+ return 'ok → Awaiting Workspace';
59
62
  case 'close':
60
63
  return kind.action === 'close' ? 'Closed' : 'Closed as not planned';
61
64
  }
@@ -81,6 +84,7 @@ export const actionToastColor = (
81
84
  case 'set_story':
82
85
  case 'set_status':
83
86
  case 'set_in_tmux_by_human':
87
+ case 'ok_and_awaiting_workspace':
84
88
  return 'blue';
85
89
  case 'close':
86
90
  return 'red';
@@ -38,6 +38,7 @@ export type ConsoleOperationHandlers = {
38
38
  onSetStatus: (option: ConsoleFieldOption) => void;
39
39
  onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
40
40
  onClose: (action: ConsoleCloseAction) => void;
41
+ onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => void;
41
42
  };
42
43
 
43
44
  export const STATUS_BUTTON_NAMES: string[] = [
@@ -74,6 +74,7 @@ const buildOperations = (): ConsoleOperationsApi => ({
74
74
  setStatus: jest.fn(async () => {}),
75
75
  setInTmuxByHuman: jest.fn(async () => {}),
76
76
  closeIssue: jest.fn(async () => {}),
77
+ okAndMoveToAwaitingWorkspace: jest.fn(async () => {}),
77
78
  addComment: jest.fn(async () => ({
78
79
  author: 'HiromiShikata',
79
80
  body: 'comment body',
@@ -150,6 +150,13 @@ export const ConsoleItemDetailContainer = ({
150
150
  commit: () => operations.closeIssue(item, action),
151
151
  });
152
152
  },
153
+ onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => {
154
+ onQueueAction({
155
+ kind: { type: 'ok_and_awaiting_workspace' },
156
+ item,
157
+ commit: () => operations.okAndMoveToAwaitingWorkspace(item, option),
158
+ });
159
+ },
153
160
  };
154
161
 
155
162
  const resolvedStoryName =