github-issue-tower-defence-management 2.11.0 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleTabNames.js +1 -0
  3. package/bin/adapter/entry-points/console/consoleTabNames.js.map +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/assets/index-D6QOQvoN.js +88 -0
  5. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  6. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +14 -0
  7. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/adapter/entry-points/console/consoleTabNames.ts +1 -0
  10. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +41 -0
  11. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +52 -2
  12. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +4 -0
  13. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +1 -0
  14. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +5 -1
  15. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +4 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +9 -2
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +37 -1
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +13 -2
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedItemRow.stories.tsx +50 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedItemRow.test.tsx +98 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedItemRow.tsx +66 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedList.stories.tsx +69 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedList.test.tsx +101 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleQueuedList.tsx +75 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.test.tsx +12 -0
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.tsx +1 -9
  27. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +1 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +71 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +2 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.test.ts +26 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.ts +6 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/logic/colors.ts +12 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.test.ts +1 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +11 -1
  35. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +58 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +25 -9
  37. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +14 -0
  38. package/src/adapter/entry-points/console/ui-dist/assets/index-D6QOQvoN.js +88 -0
  39. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  40. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +84 -0
  41. package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +39 -0
  42. package/types/adapter/entry-points/console/consoleTabNames.d.ts.map +1 -1
  43. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +13 -1
  44. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
  45. package/bin/adapter/entry-points/console/ui-dist/assets/index-BSa2JTna.js +0 -88
  46. package/src/adapter/entry-points/console/ui-dist/assets/index-BSa2JTna.js +0 -88
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { buildConsoleListRows } from '../../logic/grouping';
3
+ import {
4
+ consoleAgentOptionsFixture,
5
+ consoleListItemsFixture,
6
+ consoleStatusOptionsFixture,
7
+ consoleStoryColorsFixture,
8
+ } from '../../testing/fixtures';
9
+ import { ConsoleQueuedList } from './ConsoleQueuedList';
10
+
11
+ const queuedItems = consoleListItemsFixture.filter(
12
+ (item) =>
13
+ (item.status === 'Awaiting Workspace' || item.status === 'Preparation') &&
14
+ item.dependedIssueUrls.length === 0,
15
+ );
16
+
17
+ const meta: Meta<typeof ConsoleQueuedList> = {
18
+ title: 'Console/ConsoleQueuedList',
19
+ component: ConsoleQueuedList,
20
+ args: {
21
+ storyColors: consoleStoryColorsFixture,
22
+ statusOptions: consoleStatusOptionsFixture,
23
+ agentOptions: consoleAgentOptionsFixture,
24
+ activeItemId: null,
25
+ isLoading: false,
26
+ error: null,
27
+ onSelectItem: () => {},
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+
33
+ type Story = StoryObj<typeof ConsoleQueuedList>;
34
+
35
+ export const WithQueuedItems: Story = {
36
+ args: {
37
+ rows: buildConsoleListRows(queuedItems, {}, []),
38
+ },
39
+ };
40
+
41
+ export const WithStoryGroups: Story = {
42
+ args: {
43
+ rows: buildConsoleListRows(
44
+ queuedItems,
45
+ {},
46
+ Object.keys(consoleStoryColorsFixture),
47
+ ),
48
+ },
49
+ };
50
+
51
+ export const Loading: Story = {
52
+ args: {
53
+ rows: [],
54
+ isLoading: true,
55
+ },
56
+ };
57
+
58
+ export const Empty: Story = {
59
+ args: {
60
+ rows: [],
61
+ },
62
+ };
63
+
64
+ export const ErrorState: Story = {
65
+ args: {
66
+ rows: [],
67
+ error: 'HTTP 404',
68
+ },
69
+ };
@@ -0,0 +1,101 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { buildConsoleListRows } from '../../logic/grouping';
3
+ import {
4
+ consoleAgentOptionsFixture,
5
+ consoleListItemsFixture,
6
+ consoleStatusOptionsFixture,
7
+ consoleStoryColorsFixture,
8
+ } from '../../testing/fixtures';
9
+ import { ConsoleQueuedList } from './ConsoleQueuedList';
10
+
11
+ const queuedItems = consoleListItemsFixture.filter(
12
+ (item) =>
13
+ (item.status === 'Awaiting Workspace' || item.status === 'Preparation') &&
14
+ item.dependedIssueUrls.length === 0,
15
+ );
16
+ const rows = buildConsoleListRows(queuedItems, {}, []);
17
+
18
+ describe('ConsoleQueuedList', () => {
19
+ it('renders group headers and queued item rows', () => {
20
+ const { container } = render(
21
+ <ConsoleQueuedList
22
+ rows={rows}
23
+ storyColors={consoleStoryColorsFixture}
24
+ statusOptions={consoleStatusOptionsFixture}
25
+ agentOptions={consoleAgentOptionsFixture}
26
+ activeItemId={null}
27
+ isLoading={false}
28
+ error={null}
29
+ onSelectItem={() => {}}
30
+ />,
31
+ );
32
+ const buttons = container.querySelectorAll('button');
33
+ expect(buttons.length).toBe(queuedItems.length);
34
+ });
35
+
36
+ it('calls onSelectItem with the clicked item', () => {
37
+ const onSelectItem = jest.fn();
38
+ const { getByText } = render(
39
+ <ConsoleQueuedList
40
+ rows={rows}
41
+ storyColors={consoleStoryColorsFixture}
42
+ statusOptions={consoleStatusOptionsFixture}
43
+ agentOptions={consoleAgentOptionsFixture}
44
+ activeItemId={null}
45
+ isLoading={false}
46
+ error={null}
47
+ onSelectItem={onSelectItem}
48
+ />,
49
+ );
50
+ fireEvent.click(getByText(queuedItems[0].title));
51
+ expect(onSelectItem).toHaveBeenCalledWith(queuedItems[0]);
52
+ });
53
+
54
+ it('shows the loading state', () => {
55
+ const { getByText } = render(
56
+ <ConsoleQueuedList
57
+ rows={[]}
58
+ storyColors={{}}
59
+ statusOptions={[]}
60
+ agentOptions={[]}
61
+ activeItemId={null}
62
+ isLoading
63
+ error={null}
64
+ onSelectItem={() => {}}
65
+ />,
66
+ );
67
+ expect(getByText('Loading list...')).toBeInTheDocument();
68
+ });
69
+
70
+ it('shows the empty state', () => {
71
+ const { getByText } = render(
72
+ <ConsoleQueuedList
73
+ rows={[]}
74
+ storyColors={{}}
75
+ statusOptions={[]}
76
+ agentOptions={[]}
77
+ activeItemId={null}
78
+ isLoading={false}
79
+ error={null}
80
+ onSelectItem={() => {}}
81
+ />,
82
+ );
83
+ expect(getByText('No items')).toBeInTheDocument();
84
+ });
85
+
86
+ it('shows the error state', () => {
87
+ const { getByRole } = render(
88
+ <ConsoleQueuedList
89
+ rows={[]}
90
+ storyColors={{}}
91
+ statusOptions={[]}
92
+ agentOptions={[]}
93
+ activeItemId={null}
94
+ isLoading={false}
95
+ error="HTTP 503"
96
+ onSelectItem={() => {}}
97
+ />,
98
+ );
99
+ expect(getByRole('alert')).toHaveTextContent('HTTP 503');
100
+ });
101
+ });
@@ -0,0 +1,75 @@
1
+ import {
2
+ type ConsoleListRow,
3
+ resolveStoryColorEnum,
4
+ } from '../../logic/grouping';
5
+ import type {
6
+ ConsoleFieldOption,
7
+ ConsoleListItem,
8
+ ConsoleStoryColorSource,
9
+ } from '../../logic/types';
10
+ import { ConsoleQueuedItemRow } from './ConsoleQueuedItemRow';
11
+ import { ConsoleStorySummary } from './ConsoleStorySummary';
12
+
13
+ export type ConsoleQueuedListProps = {
14
+ rows: ConsoleListRow[];
15
+ storyColors: ConsoleStoryColorSource;
16
+ statusOptions: ConsoleFieldOption[];
17
+ agentOptions: ConsoleFieldOption[];
18
+ activeItemId: string | null;
19
+ isLoading: boolean;
20
+ error: string | null;
21
+ onSelectItem: (item: ConsoleListItem) => void;
22
+ };
23
+
24
+ export const ConsoleQueuedList = ({
25
+ rows,
26
+ storyColors,
27
+ statusOptions,
28
+ agentOptions,
29
+ activeItemId,
30
+ isLoading,
31
+ error,
32
+ onSelectItem,
33
+ }: ConsoleQueuedListProps) => {
34
+ if (error !== null) {
35
+ return (
36
+ <p role="alert" className="console-list-message console-list-error">
37
+ Failed to load list: {error}
38
+ </p>
39
+ );
40
+ }
41
+
42
+ if (isLoading) {
43
+ return <p className="console-list-message">Loading list...</p>;
44
+ }
45
+
46
+ if (rows.length === 0) {
47
+ return <p className="console-list-empty">No items</p>;
48
+ }
49
+
50
+ return (
51
+ <ul className="console-list">
52
+ {rows.map((row) =>
53
+ row.kind === 'group-header' ? (
54
+ <li key={`group:${row.story}`} className="console-list-group">
55
+ <ConsoleStorySummary
56
+ story={row.story}
57
+ count={row.count}
58
+ colorEnum={resolveStoryColorEnum(storyColors, row.story)}
59
+ />
60
+ </li>
61
+ ) : (
62
+ <li key={row.item.itemId} className="console-list-row">
63
+ <ConsoleQueuedItemRow
64
+ item={row.item}
65
+ isActive={row.item.itemId === activeItemId}
66
+ statusOptions={statusOptions}
67
+ agentOptions={agentOptions}
68
+ onSelect={onSelectItem}
69
+ />
70
+ </li>
71
+ ),
72
+ )}
73
+ </ul>
74
+ );
75
+ };
@@ -34,6 +34,18 @@ describe('ConsoleOkAndAwaitingWorkspaceActions', () => {
34
34
  expect(container.firstChild).toBeNull();
35
35
  });
36
36
 
37
+ it('applies the approve green style class regardless of the status option color', () => {
38
+ const { getByRole } = render(
39
+ <ConsoleOkAndAwaitingWorkspaceActions
40
+ statusOptions={consoleStatusOptionsFixture}
41
+ onOkAndAwaitingWorkspace={() => {}}
42
+ />,
43
+ );
44
+ expect(
45
+ getByRole('button', { name: 'ok & Awaiting Workspace' }),
46
+ ).toHaveClass('console-op-button-approve');
47
+ });
48
+
37
49
  it('calls the callback with the awaiting workspace option on click', () => {
38
50
  const onOkAndAwaitingWorkspace = jest.fn();
39
51
  const { getByRole } = render(
@@ -1,4 +1,3 @@
1
- import { colorFromEnum } from '../../logic/colors';
2
1
  import type { ConsoleFieldOption } from '../../logic/types';
3
2
 
4
3
  export type ConsoleOkAndAwaitingWorkspaceActionsProps = {
@@ -20,18 +19,11 @@ export const ConsoleOkAndAwaitingWorkspaceActions = ({
20
19
  return null;
21
20
  }
22
21
 
23
- const palette = colorFromEnum(option.color);
24
-
25
22
  return (
26
23
  <div className="console-op-group">
27
24
  <button
28
25
  type="button"
29
- className="console-op-button"
30
- style={{
31
- color: palette.fg,
32
- borderColor: palette.border,
33
- backgroundColor: palette.bg,
34
- }}
26
+ className="console-op-button console-op-button-approve"
35
27
  onClick={() => onOkAndAwaitingWorkspace(option)}
36
28
  >
37
29
  ok &amp; Awaiting Workspace
@@ -14,6 +14,7 @@ const counts = (
14
14
  'failed-preparation': 0,
15
15
  'todo-by-human': 0,
16
16
  'todo-by-agent': 0,
17
+ queued: 0,
17
18
  stories: 0,
18
19
  ...overrides,
19
20
  });
@@ -272,6 +272,7 @@ describe('useConsoleTabData', () => {
272
272
  prs: {
273
273
  generatedAt: '2026-08-01T10:00:00Z',
274
274
  statusOptions: [],
275
+ agentOptions: [],
275
276
  storyOptions: [],
276
277
  storyColors: {},
277
278
  stories: [],
@@ -303,6 +304,7 @@ describe('useConsoleTabData', () => {
303
304
  'workflow-blocker': {
304
305
  generatedAt: '2026-08-01T10:00:00Z',
305
306
  statusOptions: [],
307
+ agentOptions: [],
306
308
  storyOptions: [],
307
309
  storyColors: {},
308
310
  items: [],
@@ -314,6 +316,7 @@ describe('useConsoleTabData', () => {
314
316
  'failed-preparation': {
315
317
  generatedAt: '2026-08-01T10:00:00Z',
316
318
  statusOptions: [],
319
+ agentOptions: [],
317
320
  storyOptions: [],
318
321
  storyColors: {},
319
322
  items: [],
@@ -325,6 +328,7 @@ describe('useConsoleTabData', () => {
325
328
  'todo-by-human': {
326
329
  generatedAt: '2026-08-01T10:00:00Z',
327
330
  statusOptions: [],
331
+ agentOptions: [],
328
332
  storyOptions: [],
329
333
  storyColors: {},
330
334
  items: [],
@@ -336,6 +340,19 @@ describe('useConsoleTabData', () => {
336
340
  'todo-by-agent': {
337
341
  generatedAt: '2026-08-01T10:00:00Z',
338
342
  statusOptions: [],
343
+ agentOptions: [],
344
+ storyOptions: [],
345
+ storyColors: {},
346
+ items: [],
347
+ stories: [],
348
+ defaultNameWithOwner: null,
349
+ fromCache: false,
350
+ storyOrder: [],
351
+ },
352
+ queued: {
353
+ generatedAt: '2026-08-01T10:00:00Z',
354
+ statusOptions: [],
355
+ agentOptions: [],
339
356
  storyOptions: [],
340
357
  storyColors: {},
341
358
  items: [],
@@ -347,6 +364,7 @@ describe('useConsoleTabData', () => {
347
364
  stories: {
348
365
  generatedAt: '2026-08-01T10:00:00Z',
349
366
  statusOptions: [],
367
+ agentOptions: [],
350
368
  storyOptions: [],
351
369
  storyColors: {},
352
370
  items: [],
@@ -390,6 +408,7 @@ describe('useConsoleTabData', () => {
390
408
  prs: {
391
409
  generatedAt: '2026-08-01T10:00:00Z',
392
410
  statusOptions: [],
411
+ agentOptions: [],
393
412
  storyOptions: [],
394
413
  storyColors: {},
395
414
  items: [],
@@ -401,6 +420,7 @@ describe('useConsoleTabData', () => {
401
420
  'workflow-blocker': {
402
421
  generatedAt: '2026-08-01T10:00:00Z',
403
422
  statusOptions: [],
423
+ agentOptions: [],
404
424
  storyOptions: [],
405
425
  storyColors: {},
406
426
  items: [],
@@ -412,6 +432,7 @@ describe('useConsoleTabData', () => {
412
432
  'failed-preparation': {
413
433
  generatedAt: '2026-08-01T10:00:00Z',
414
434
  statusOptions: [],
435
+ agentOptions: [],
415
436
  storyOptions: [],
416
437
  storyColors: {},
417
438
  items: [],
@@ -423,6 +444,7 @@ describe('useConsoleTabData', () => {
423
444
  'todo-by-human': {
424
445
  generatedAt: '2026-08-01T10:00:00Z',
425
446
  statusOptions: [],
447
+ agentOptions: [],
426
448
  storyOptions: [],
427
449
  storyColors: {},
428
450
  items: [],
@@ -434,6 +456,19 @@ describe('useConsoleTabData', () => {
434
456
  'todo-by-agent': {
435
457
  generatedAt: '2026-08-01T10:00:00Z',
436
458
  statusOptions: [],
459
+ agentOptions: [],
460
+ storyOptions: [],
461
+ storyColors: {},
462
+ items: [],
463
+ stories: [],
464
+ defaultNameWithOwner: null,
465
+ fromCache: false,
466
+ storyOrder: [],
467
+ },
468
+ queued: {
469
+ generatedAt: '2026-08-01T10:00:00Z',
470
+ statusOptions: [],
471
+ agentOptions: [],
437
472
  storyOptions: [],
438
473
  storyColors: {},
439
474
  items: [],
@@ -445,6 +480,7 @@ describe('useConsoleTabData', () => {
445
480
  stories: {
446
481
  generatedAt: '2026-08-01T10:00:00Z',
447
482
  statusOptions: [],
483
+ agentOptions: [],
448
484
  storyOptions: [],
449
485
  storyColors: {},
450
486
  items: [],
@@ -472,4 +508,39 @@ describe('useConsoleTabData', () => {
472
508
  expect(fetchMock).not.toHaveBeenCalled();
473
509
  jest.useRealTimers();
474
510
  });
511
+
512
+ it('parses agentOptions from the snapshot payload', async () => {
513
+ const agentOptions = [
514
+ { id: 'ag1', name: 'developer', color: 'GRAY' },
515
+ { id: 'ag2', name: 'chore', color: 'GRAY' },
516
+ ];
517
+ const fetchMock = jest.fn(async (url: string) => ({
518
+ ok: true,
519
+ status: 200,
520
+ json: async () => ({
521
+ pjcode: 'acme',
522
+ generatedAt: '2026-06-19T00:00:00.000Z',
523
+ statusOptions: [],
524
+ storyColors: {},
525
+ items: [],
526
+ agentOptions: url.includes('/queued/') ? agentOptions : [],
527
+ }),
528
+ }));
529
+ global.fetch = fetchMock as unknown as typeof fetch;
530
+
531
+ const { result } = renderHook(() => useConsoleTabData('acme'));
532
+ await waitFor(() => {
533
+ expect(result.current.isLoading).toBe(false);
534
+ });
535
+ expect(result.current.snapshots.queued?.agentOptions).toEqual(agentOptions);
536
+ });
537
+
538
+ it('defaults agentOptions to empty array when missing from payload', async () => {
539
+ installNetworkFetch();
540
+ const { result } = renderHook(() => useConsoleTabData('acme'));
541
+ await waitFor(() => {
542
+ expect(result.current.isLoading).toBe(false);
543
+ });
544
+ expect(result.current.snapshots.prs?.agentOptions).toEqual([]);
545
+ });
475
546
  });
@@ -13,6 +13,7 @@ export type ConsoleTabSnapshot = {
13
13
  items: ConsoleListItem[];
14
14
  generatedAt: string;
15
15
  statusOptions: ConsoleFieldOption[];
16
+ agentOptions: ConsoleFieldOption[];
16
17
  storyOptions: ConsoleFieldOption[];
17
18
  storyColors: ConsoleStoryColorSource;
18
19
  stories: ConsoleStoryEntry[];
@@ -73,6 +74,7 @@ const parseSnapshotData = (
73
74
  ? payload.generatedAt
74
75
  : '',
75
76
  statusOptions: isRecord(payload) ? parseOptions(payload.statusOptions) : [],
77
+ agentOptions: isRecord(payload) ? parseOptions(payload.agentOptions) : [],
76
78
  storyOptions: isRecord(payload) ? parseOptions(payload.storyOptions) : [],
77
79
  storyColors:
78
80
  isRecord(payload) && isRecord(payload.storyColors)
@@ -76,6 +76,32 @@ describe('airplaneSnapshot', () => {
76
76
  expect(result?.tabs.acme?.prs.statusOptions).toHaveLength(1);
77
77
  expect(result?.tabs.acme?.prs.fromCache).toBe(false);
78
78
  expect(result?.tabs.acme?.prs.storyOrder).toEqual([]);
79
+ expect(result?.tabs.acme?.prs.agentOptions).toEqual([]);
80
+ });
81
+
82
+ it('parses agentOptions from a tab snapshot', () => {
83
+ const raw = {
84
+ capturedAt: '2026-01-01T00:00:00Z',
85
+ tabs: {
86
+ acme: {
87
+ prs: {
88
+ generatedAt: '2026-01-01T00:00:00Z',
89
+ statusOptions: [],
90
+ agentOptions: [{ id: 'ag1', name: 'developer', color: 'GRAY' }],
91
+ storyOptions: [],
92
+ storyColors: {},
93
+ items: [],
94
+ },
95
+ },
96
+ },
97
+ items: {},
98
+ failures: [],
99
+ };
100
+
101
+ const result = parseAirplaneSnapshot(raw);
102
+ expect(result?.tabs.acme?.prs.agentOptions).toEqual([
103
+ { id: 'ag1', name: 'developer', color: 'GRAY' },
104
+ ]);
79
105
  });
80
106
 
81
107
  it('parses storyOrder and fromCache from a tab snapshot', () => {
@@ -16,6 +16,7 @@ export type AirplaneTabSnapshot = {
16
16
  items: ConsoleListItem[];
17
17
  generatedAt: string;
18
18
  statusOptions: ConsoleFieldOption[];
19
+ agentOptions: ConsoleFieldOption[];
19
20
  storyOptions: ConsoleFieldOption[];
20
21
  storyColors: ConsoleStoryColorSource;
21
22
  stories: ConsoleStoryEntry[];
@@ -170,6 +171,7 @@ const parseTabSnapshot = (payload: unknown): AirplaneTabSnapshot => {
170
171
  items: [],
171
172
  generatedAt: '',
172
173
  statusOptions: [],
174
+ agentOptions: [],
173
175
  storyOptions: [],
174
176
  storyColors: {},
175
177
  stories: [],
@@ -188,6 +190,9 @@ const parseTabSnapshot = (payload: unknown): AirplaneTabSnapshot => {
188
190
  )
189
191
  ? (payload.statusOptions.filter(isRecord) as ConsoleFieldOption[])
190
192
  : [];
193
+ const agentOptions: ConsoleFieldOption[] = Array.isArray(payload.agentOptions)
194
+ ? (payload.agentOptions.filter(isRecord) as ConsoleFieldOption[])
195
+ : [];
191
196
  const storyOptions: ConsoleFieldOption[] = Array.isArray(payload.storyOptions)
192
197
  ? (payload.storyOptions.filter(isRecord) as ConsoleFieldOption[])
193
198
  : [];
@@ -202,6 +207,7 @@ const parseTabSnapshot = (payload: unknown): AirplaneTabSnapshot => {
202
207
  generatedAt:
203
208
  typeof payload.generatedAt === 'string' ? payload.generatedAt : '',
204
209
  statusOptions,
210
+ agentOptions,
205
211
  storyOptions,
206
212
  storyColors: isRecord(payload.storyColors)
207
213
  ? (payload.storyColors as ConsoleStoryColorSource)
@@ -71,3 +71,15 @@ export const colorFromEnum = (
71
71
  }
72
72
  return CONSOLE_COLOR_PALETTE.GRAY;
73
73
  };
74
+
75
+ export const badgeStyle = (colorEnum: string | null) => {
76
+ const palette = colorFromEnum(colorEnum);
77
+ return {
78
+ backgroundColor: palette.bg,
79
+ borderColor: palette.border,
80
+ color: palette.fg,
81
+ border: '1px solid' as const,
82
+ borderRadius: '6px',
83
+ padding: '0 6px',
84
+ };
85
+ };
@@ -12,6 +12,7 @@ const counts = (
12
12
  'failed-preparation': 0,
13
13
  'todo-by-human': 0,
14
14
  'todo-by-agent': 0,
15
+ queued: 0,
15
16
  stories: 0,
16
17
  ...overrides,
17
18
  });
@@ -43,7 +43,15 @@ export type ConsoleStatusTab = {
43
43
  items: ConsoleListItem[];
44
44
  };
45
45
 
46
- export type ConsoleTabData = ConsoleStatusTab;
46
+ export type ConsoleQueuedTab = {
47
+ pjcode: string;
48
+ generatedAt: string;
49
+ statusOptions: ConsoleFieldOption[];
50
+ agentOptions: ConsoleFieldOption[];
51
+ storyOrder: string[];
52
+ storyColors: Record<string, { color: ConsoleColor }>;
53
+ items: ConsoleListItem[];
54
+ };
47
55
 
48
56
  export type ConsoleStoryColorSource = Record<
49
57
  string,
@@ -138,6 +146,7 @@ export type ConsoleTabName =
138
146
  | 'failed-preparation'
139
147
  | 'todo-by-human'
140
148
  | 'todo-by-agent'
149
+ | 'queued'
141
150
  | 'stories';
142
151
 
143
152
  export type ConsoleStoryEntry = {
@@ -159,5 +168,6 @@ export const CONSOLE_TABS: ConsoleTab[] = [
159
168
  { name: 'failed-preparation', label: 'Failed Preparation' },
160
169
  { name: 'todo-by-human', label: 'Todo by human' },
161
170
  { name: 'todo-by-agent', label: 'Todo by agent' },
171
+ { name: 'queued', label: 'Queued' },
162
172
  { name: 'stories', label: 'Stories' },
163
173
  ];
@@ -1083,6 +1083,64 @@ describe('ConsolePage auto-advance tab', () => {
1083
1083
  });
1084
1084
  });
1085
1085
 
1086
+ it('shows the queued tab badge count when queued items are present', async () => {
1087
+ const queuedItem = {
1088
+ number: 900,
1089
+ title: 'Queued task waiting',
1090
+ url: 'https://github.com/o/r/issues/900',
1091
+ repo: 'o/r',
1092
+ nameWithOwner: 'o/r',
1093
+ projectItemId: 'PVTI_900',
1094
+ itemId: 'PVTI_900',
1095
+ isPr: false,
1096
+ relatedOpenPullRequestUrls: [],
1097
+ story: 'TDPM Console port',
1098
+ status: 'Awaiting Workspace',
1099
+ nextActionDate: null,
1100
+ nextActionHour: null,
1101
+ dependedIssueUrls: [],
1102
+ labels: [],
1103
+ createdAt: '2026-06-17T00:00:00.000Z',
1104
+ };
1105
+ global.fetch = jest.fn(async (url: string) => {
1106
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
1107
+ if (listMatch !== null) {
1108
+ const tab = listMatch[1];
1109
+ if (tab === 'queued') {
1110
+ return {
1111
+ ok: true,
1112
+ status: 200,
1113
+ json: async () => ({
1114
+ ...listPayload(''),
1115
+ agentOptions: [],
1116
+ items: [queuedItem],
1117
+ }),
1118
+ };
1119
+ }
1120
+ return { ok: true, status: 200, json: async () => listPayload(tab) };
1121
+ }
1122
+ if (url === '/api/projects') {
1123
+ return {
1124
+ ok: true,
1125
+ status: 200,
1126
+ json: async () => ({ pjcodes: ['acme'] }),
1127
+ };
1128
+ }
1129
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
1130
+ }) as unknown as typeof fetch;
1131
+
1132
+ const { container } = render(<ConsolePage />);
1133
+
1134
+ await waitFor(() => {
1135
+ const tabs = [...container.querySelectorAll('.console-tab')];
1136
+ const queuedTab = tabs.find((el) => el.textContent?.includes('Queued'));
1137
+ expect(queuedTab).toBeTruthy();
1138
+ expect(queuedTab?.querySelector('.console-tab-badge')?.textContent).toBe(
1139
+ '1',
1140
+ );
1141
+ });
1142
+ });
1143
+
1086
1144
  it('reads console-story-show-gray from localStorage and shows gray stories when true', async () => {
1087
1145
  localStorage.setItem('console-story-show-gray', 'true');
1088
1146
  window.history.replaceState({}, '', '/projects/acme/stories?k=token');