github-issue-tower-defence-management 1.196.1 → 1.197.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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/bin/adapter/entry-points/cli/index.js +66 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleDataDelivery.js +1 -20
  8. package/bin/adapter/entry-points/console/consoleDataDelivery.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CYwLv9pf.js → index-lT5OKx7e.js} +2 -2
  10. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  11. package/bin/domain/usecases/CliErrorReportUseCase.js +50 -0
  12. package/bin/domain/usecases/CliErrorReportUseCase.js.map +1 -0
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/cli/index.ts +85 -0
  15. package/src/adapter/entry-points/cli/projectConfig.ts +14 -0
  16. package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +12 -6
  17. package/src/adapter/entry-points/console/consoleDataDelivery.ts +1 -31
  18. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +30 -18
  19. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +12 -3
  20. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +3 -1
  21. package/src/adapter/entry-points/console/ui-dist/assets/{index-CYwLv9pf.js → index-lT5OKx7e.js} +2 -2
  22. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  23. package/src/adapter/entry-points/console/webServer.test.ts +10 -4
  24. package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +2 -2
  25. package/src/domain/usecases/CliErrorReportUseCase.test.ts +151 -0
  26. package/src/domain/usecases/CliErrorReportUseCase.ts +72 -0
  27. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  28. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  29. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  30. package/types/adapter/entry-points/console/consoleDataDelivery.d.ts.map +1 -1
  31. package/types/domain/usecases/CliErrorReportUseCase.d.ts +14 -0
  32. package/types/domain/usecases/CliErrorReportUseCase.d.ts.map +1 -0
@@ -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-CYwLv9pf.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-lT5OKx7e.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CliErrorReportUseCase = void 0;
4
+ class CliErrorReportUseCase {
5
+ constructor(issueRepository) {
6
+ this.issueRepository = issueRepository;
7
+ this.run = async (params) => {
8
+ const { error, owner, repo, commandLine } = params;
9
+ const errorName = error instanceof Error ? (error.name ?? 'Error') : 'Error';
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ const stack = error instanceof Error && error.stack ? error.stack : String(error);
12
+ const title = `CLI error: ${errorName}: ${message.slice(0, 80)}`;
13
+ const occurredAt = new Date().toISOString();
14
+ const buildBody = (prefix) => [
15
+ `${prefix}`,
16
+ ``,
17
+ `Error name: ${errorName}`,
18
+ `Message: ${message}`,
19
+ `Command: \`${commandLine}\``,
20
+ `Occurred at: ${occurredAt}`,
21
+ ``,
22
+ `Stack trace:`,
23
+ `\`\`\``,
24
+ stack,
25
+ `\`\`\``,
26
+ ].join('\n');
27
+ try {
28
+ const results = await this.issueRepository.searchIssue({
29
+ owner,
30
+ repositoryName: repo,
31
+ type: 'issue',
32
+ state: 'open',
33
+ title,
34
+ });
35
+ const existing = results.find((r) => r.title === title);
36
+ if (existing) {
37
+ await this.issueRepository.createCommentByUrl(existing.url, buildBody('CLI error recurrence'));
38
+ }
39
+ else {
40
+ await this.issueRepository.createNewIssue(owner, repo, title, buildBody('CLI error report'), [], []);
41
+ }
42
+ }
43
+ catch (reportError) {
44
+ console.error('CliErrorReportUseCase: failed to report error:', reportError);
45
+ }
46
+ };
47
+ }
48
+ }
49
+ exports.CliErrorReportUseCase = CliErrorReportUseCase;
50
+ //# sourceMappingURL=CliErrorReportUseCase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CliErrorReportUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/CliErrorReportUseCase.ts"],"names":[],"mappings":";;;AAOA,MAAa,qBAAqB;IAChC,YAA6B,eAAyC;QAAzC,oBAAe,GAAf,eAAe,CAA0B;QAEtE,QAAG,GAAG,KAAK,EAAE,MAKZ,EAAiB,EAAE;YAClB,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;YACnD,MAAM,SAAS,GACb,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7D,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,KAAK,GACT,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,cAAc,SAAS,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;YACjE,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAE5C,MAAM,SAAS,GAAG,CAAC,MAAc,EAAU,EAAE,CAC3C;gBACE,GAAG,MAAM,EAAE;gBACX,EAAE;gBACF,eAAe,SAAS,EAAE;gBAC1B,YAAY,OAAO,EAAE;gBACrB,cAAc,WAAW,IAAI;gBAC7B,gBAAgB,UAAU,EAAE;gBAC5B,EAAE;gBACF,cAAc;gBACd,QAAQ;gBACR,KAAK;gBACL,QAAQ;aACT,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEf,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;oBACrD,KAAK;oBACL,cAAc,EAAE,IAAI;oBACpB,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,MAAM;oBACb,KAAK;iBACN,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;gBACxD,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,IAAI,CAAC,eAAe,CAAC,kBAAkB,CAC3C,QAAQ,CAAC,GAAG,EACZ,SAAS,CAAC,sBAAsB,CAAC,CAClC,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,CAAC,eAAe,CAAC,cAAc,CACvC,KAAK,EACL,IAAI,EACJ,KAAK,EACL,SAAS,CAAC,kBAAkB,CAAC,EAC7B,EAAE,EACF,EAAE,CACH,CAAC;gBACJ,CAAC;YACH,CAAC;YAAC,OAAO,WAAW,EAAE,CAAC;gBACrB,OAAO,CAAC,KAAK,CACX,gDAAgD,EAChD,WAAW,CACZ,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;IA9DuE,CAAC;CA+D3E;AAhED,sDAgEC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.196.1",
3
+ "version": "1.197.1",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -13,6 +13,7 @@ export {
13
13
  import * as path from 'path';
14
14
  import type { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
15
15
  import { CheckIssueReviewReadinessUseCase } from '../../../domain/usecases/CheckIssueReviewReadinessUseCase';
16
+ import { CliErrorReportUseCase } from '../../../domain/usecases/CliErrorReportUseCase';
16
17
  import { assertDashboardDisplayLabelsUnique } from '../../../domain/usecases/dashboard/DashboardProjectCode';
17
18
  import { isOwnerCallCalledAtValid } from '../../../domain/usecases/intmux/OwnerCallFile';
18
19
  import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
@@ -362,6 +363,11 @@ program
362
363
  readmeOverrides,
363
364
  );
364
365
 
366
+ if (config.errorReportingRepository) {
367
+ process.env.TDPM_ERROR_REPORT_REPOSITORY =
368
+ config.errorReportingRepository;
369
+ }
370
+
365
371
  const projectUrl = config.projectUrl;
366
372
  const defaultAgentName = config.defaultAgentName;
367
373
  const manager = config.manager;
@@ -605,6 +611,11 @@ program
605
611
  readmeOverrides,
606
612
  );
607
613
 
614
+ if (config.errorReportingRepository) {
615
+ process.env.TDPM_ERROR_REPORT_REPOSITORY =
616
+ config.errorReportingRepository;
617
+ }
618
+
608
619
  const projectUrl = config.projectUrl;
609
620
 
610
621
  if (!projectUrl) {
@@ -750,6 +761,11 @@ program
750
761
  readmeOverrides,
751
762
  );
752
763
 
764
+ if (config.errorReportingRepository) {
765
+ process.env.TDPM_ERROR_REPORT_REPOSITORY =
766
+ config.errorReportingRepository;
767
+ }
768
+
753
769
  const projectName = config.projectName ?? 'default';
754
770
  const localStorageRepository = new LocalStorageRepository();
755
771
  const cachePath = projectCacheDirectory(projectName);
@@ -1350,13 +1366,82 @@ export const reportFatalErrorAndExit = (error: unknown): void => {
1350
1366
  process.exit(1);
1351
1367
  };
1352
1368
 
1369
+ const buildCliErrorReporter = (): {
1370
+ useCase: CliErrorReportUseCase;
1371
+ owner: string;
1372
+ repo: string;
1373
+ } | null => {
1374
+ const token = process.env.GH_TOKEN;
1375
+ const targetRepo = process.env.TDPM_ERROR_REPORT_REPOSITORY;
1376
+ if (!token || !targetRepo) {
1377
+ return null;
1378
+ }
1379
+ const slashIndex = targetRepo.indexOf('/');
1380
+ if (slashIndex <= 0 || slashIndex === targetRepo.length - 1) {
1381
+ return null;
1382
+ }
1383
+ const owner = targetRepo.slice(0, slashIndex);
1384
+ const repo = targetRepo.slice(slashIndex + 1);
1385
+ const localStorageRepository = new LocalStorageRepository();
1386
+ const githubRepositoryParams = buildGithubRepositoryParams(
1387
+ localStorageRepository,
1388
+ token,
1389
+ );
1390
+ const apiV3IssueRepository = new ApiV3IssueRepository(
1391
+ ...githubRepositoryParams,
1392
+ );
1393
+ const restIssueRepository = new RestIssueRepository(
1394
+ ...githubRepositoryParams,
1395
+ );
1396
+ const useCase = new CliErrorReportUseCase({
1397
+ searchIssue: apiV3IssueRepository.searchIssue.bind(apiV3IssueRepository),
1398
+ createNewIssue:
1399
+ restIssueRepository.createNewIssue.bind(restIssueRepository),
1400
+ createCommentByUrl: async (issueOrPrUrl: string, commentBody: string) => {
1401
+ await restIssueRepository.createComment(issueOrPrUrl, commentBody);
1402
+ },
1403
+ });
1404
+ return { useCase, owner, repo };
1405
+ };
1406
+
1353
1407
  export const runCliProgram = async (
1354
1408
  argv: string[],
1355
1409
  handleFatalError: (error: unknown) => void,
1356
1410
  ): Promise<void> => {
1411
+ let reported = false;
1412
+
1413
+ const safeReport = async (error: unknown): Promise<void> => {
1414
+ if (reported) {
1415
+ return;
1416
+ }
1417
+ reported = true;
1418
+ const reporter = buildCliErrorReporter();
1419
+ if (!reporter) {
1420
+ return;
1421
+ }
1422
+ await reporter.useCase.run({
1423
+ error,
1424
+ owner: reporter.owner,
1425
+ repo: reporter.repo,
1426
+ commandLine: argv.join(' '),
1427
+ });
1428
+ };
1429
+
1430
+ process.on('uncaughtException', (error: Error) => {
1431
+ void safeReport(error).then(() => {
1432
+ reportFatalErrorAndExit(error);
1433
+ });
1434
+ });
1435
+ process.on('unhandledRejection', (reason: unknown) => {
1436
+ void safeReport(reason).then(() => {
1437
+ reportFatalErrorAndExit(reason);
1438
+ });
1439
+ });
1440
+
1357
1441
  try {
1358
1442
  await program.parseAsync(argv);
1359
1443
  } catch (error) {
1444
+ await safeReport(error);
1360
1445
  handleFatalError(error);
1361
1446
  }
1362
1447
  };
@@ -33,6 +33,7 @@ export type ConfigFile = {
33
33
  consoleGithubTokenFileDir?: string;
34
34
  disks?: DiskConfig[];
35
35
  ownerApprovalTimeoutCycles?: number;
36
+ errorReportingRepository?: string;
36
37
  };
37
38
 
38
39
  export type DiskConfig = {
@@ -139,6 +140,7 @@ const knownProjectReadmeConfigKeys = [
139
140
  'labelsNotRequiringPullRequest',
140
141
  'changeTargetPathAliases',
141
142
  'consoleDataOutputDir',
143
+ 'errorReportingRepository',
142
144
  ] as const;
143
145
 
144
146
  export const loadConfigFile = (configFilePath: string): ConfigFile => {
@@ -211,6 +213,10 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
211
213
  parsed,
212
214
  'ownerApprovalTimeoutCycles',
213
215
  ),
216
+ errorReportingRepository: getStringValue(
217
+ parsed,
218
+ 'errorReportingRepository',
219
+ ),
214
220
  };
215
221
  } catch (error) {
216
222
  const message = error instanceof Error ? error.message : String(error);
@@ -297,6 +303,10 @@ export const parseProjectReadmeConfig = (
297
303
  'changeTargetPathAliases',
298
304
  ),
299
305
  consoleDataOutputDir: getStringValue(parsed, 'consoleDataOutputDir'),
306
+ errorReportingRepository: getStringValue(
307
+ parsed,
308
+ 'errorReportingRepository',
309
+ ),
300
310
  };
301
311
  } catch {
302
312
  console.warn('Failed to parse YAML from project README config section');
@@ -403,6 +413,10 @@ export const mergeConfigs = (
403
413
  ownerApprovalTimeoutCycles:
404
414
  cliOverrides.ownerApprovalTimeoutCycles ??
405
415
  configFile.ownerApprovalTimeoutCycles,
416
+ errorReportingRepository:
417
+ readmeOverrides.errorReportingRepository ??
418
+ cliOverrides.errorReportingRepository ??
419
+ configFile.errorReportingRepository,
406
420
  });
407
421
 
408
422
  type GraphqlProjectV2ReadmeResponse = {
@@ -111,12 +111,12 @@ describe('buildConsoleDataResponse', () => {
111
111
  expect(response.statusCode).toBe(404);
112
112
  });
113
113
 
114
- it('serves a list file and applies the done exclusion', () => {
114
+ it('serves a list file without filtering items that have done records', () => {
115
115
  writeJson('acme/prs/list.json', {
116
116
  pjcode: 'acme',
117
117
  items: [
118
118
  { projectItemId: 'PVTI_1', title: 'keep' },
119
- { projectItemId: 'PVTI_2', title: 'drop' },
119
+ { projectItemId: 'PVTI_2', title: 'also keep despite done record' },
120
120
  ],
121
121
  });
122
122
  recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
@@ -129,16 +129,19 @@ describe('buildConsoleDataResponse', () => {
129
129
  const parsed: unknown = JSON.parse(response.body);
130
130
  expect(parsed).toEqual({
131
131
  pjcode: 'acme',
132
- items: [{ projectItemId: 'PVTI_1', title: 'keep' }],
132
+ items: [
133
+ { projectItemId: 'PVTI_1', title: 'keep' },
134
+ { projectItemId: 'PVTI_2', title: 'also keep despite done record' },
135
+ ],
133
136
  });
134
137
  });
135
138
 
136
- it('applies the done exclusion to the workflow-blocker list', () => {
139
+ it('serves the workflow-blocker list without filtering items that have done records', () => {
137
140
  writeJson('acme/workflow-blocker/list.json', {
138
141
  pjcode: 'acme',
139
142
  items: [
140
143
  { projectItemId: 'PVTI_1', title: 'keep' },
141
- { projectItemId: 'PVTI_2', title: 'processed blocker' },
144
+ { projectItemId: 'PVTI_2', title: 'also keep despite done record' },
142
145
  ],
143
146
  });
144
147
  recordDoneProjectItemId(baseDir, 'acme', 'workflow-blocker', 'PVTI_2');
@@ -151,7 +154,10 @@ describe('buildConsoleDataResponse', () => {
151
154
  const parsed: unknown = JSON.parse(response.body);
152
155
  expect(parsed).toEqual({
153
156
  pjcode: 'acme',
154
- items: [{ projectItemId: 'PVTI_1', title: 'keep' }],
157
+ items: [
158
+ { projectItemId: 'PVTI_1', title: 'keep' },
159
+ { projectItemId: 'PVTI_2', title: 'also keep despite done record' },
160
+ ],
155
161
  });
156
162
  });
157
163
 
@@ -1,6 +1,5 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { readDoneProjectItemIds } from './consoleDoneStore';
4
3
  import { CONSOLE_LIST_TAB_NAMES } from './consoleTabNames';
5
4
 
6
5
  export { CONSOLE_LIST_TAB_NAMES };
@@ -55,9 +54,6 @@ export const parseConsoleDataRoute = (
55
54
  return null;
56
55
  };
57
56
 
58
- const isRecord = (value: unknown): value is Record<string, unknown> =>
59
- value !== null && typeof value === 'object' && !Array.isArray(value);
60
-
61
57
  type ReadJsonResult = { found: false } | { found: true; data: unknown };
62
58
 
63
59
  const readJsonFile = (filePath: string): ReadJsonResult => {
@@ -75,26 +71,6 @@ const readJsonFile = (filePath: string): ReadJsonResult => {
75
71
  return { found: true, data };
76
72
  };
77
73
 
78
- const isExcludedItem = (item: unknown, doneSet: Set<string>): boolean => {
79
- if (!isRecord(item)) {
80
- return false;
81
- }
82
- const projectItemId = item.projectItemId;
83
- return typeof projectItemId === 'string' && doneSet.has(projectItemId);
84
- };
85
-
86
- const applyDoneExclusion = (
87
- listData: unknown,
88
- doneProjectItemIds: string[],
89
- ): unknown => {
90
- if (!isRecord(listData) || !Array.isArray(listData.items)) {
91
- return listData;
92
- }
93
- const doneSet = new Set(doneProjectItemIds);
94
- const items = listData.items.filter((item) => !isExcludedItem(item, doneSet));
95
- return { ...listData, items };
96
- };
97
-
98
74
  export type ConsoleDataResponse = {
99
75
  statusCode: number;
100
76
  contentType: string;
@@ -116,13 +92,7 @@ export const buildConsoleDataResponse = (
116
92
  if (!listResult.found) {
117
93
  return notFoundJson();
118
94
  }
119
- const doneProjectItemIds = readDoneProjectItemIds(
120
- consoleDataOutputDir,
121
- route.pjcode,
122
- route.tab,
123
- );
124
- const filtered = applyDoneExclusion(listResult.data, doneProjectItemIds);
125
- return okJson(filtered);
95
+ return okJson(listResult.data);
126
96
  }
127
97
  if (route.kind === 'detail') {
128
98
  const filePath = path.join(
@@ -81,33 +81,33 @@ describe('counts driven to zero do not revive on tab switch', () => {
81
81
  const overlay: ConsoleOverlay = {
82
82
  PVTI_1: { ts: 500, mode: 'prs', done: true },
83
83
  };
84
- expect(countPendingItems([item(1)], overlay)).toBe(0);
84
+ expect(countPendingItems([item(1)], overlay, 'prs')).toBe(0);
85
85
  });
86
86
 
87
- it('keeps a done item subtracted from every tab regardless of its mode', () => {
87
+ it('does not subtract a done item from a tab other than the one it was processed in', () => {
88
88
  const overlay: ConsoleOverlay = {
89
89
  PVTI_1: { ts: 100, mode: 'triage', done: true },
90
90
  };
91
- expect(countPendingItems([item(1)], overlay)).toBe(0);
91
+ expect(countPendingItems([item(1)], overlay, 'prs')).toBe(1);
92
92
  });
93
93
 
94
94
  it('does not revive a done item even when an entry was processed before the snapshot it still appears in', () => {
95
95
  const overlay: ConsoleOverlay = {
96
96
  PVTI_1: { ts: 100, mode: 'prs', done: true },
97
97
  };
98
- expect(countPendingItems([item(1)], overlay)).toBe(0);
98
+ expect(countPendingItems([item(1)], overlay, 'prs')).toBe(0);
99
99
  });
100
100
 
101
- it('does not revive a done item when it appears in a tab other than the one it was processed in', () => {
101
+ it('shows a done item in a tab other than the one it was processed in', () => {
102
102
  const overlay: ConsoleOverlay = {
103
103
  PVTI_1: { ts: 100, mode: 'prs', done: true },
104
104
  };
105
- expect(countPendingItems([item(1)], overlay)).toBe(0);
106
- expect(filterPendingItems([item(1)], overlay)).toEqual([]);
105
+ expect(countPendingItems([item(1)], overlay, 'triage')).toBe(1);
106
+ expect(filterPendingItems([item(1)], overlay, 'triage')).toEqual([item(1)]);
107
107
  });
108
108
 
109
109
  it('counts an item that has no done entry', () => {
110
- expect(countPendingItems([item(1)], {})).toBe(1);
110
+ expect(countPendingItems([item(1)], {}, 'prs')).toBe(1);
111
111
  });
112
112
  });
113
113
 
@@ -116,7 +116,7 @@ describe('filterPendingItems', () => {
116
116
  const overlay: ConsoleOverlay = {
117
117
  PVTI_1: { ts: 500, mode: 'prs', done: true },
118
118
  };
119
- const result = filterPendingItems([item(1), item(2)], overlay);
119
+ const result = filterPendingItems([item(1), item(2)], overlay, 'prs');
120
120
  expect(result.map((entry) => entry.number)).toEqual([2]);
121
121
  });
122
122
 
@@ -125,8 +125,8 @@ describe('filterPendingItems', () => {
125
125
  PVTI_1: { ts: 500, mode: 'prs', done: true },
126
126
  };
127
127
  const items = [item(1), item(2)];
128
- expect(countPendingItems(items, overlay)).toBe(
129
- filterPendingItems(items, overlay).length,
128
+ expect(countPendingItems(items, overlay, 'prs')).toBe(
129
+ filterPendingItems(items, overlay, 'prs').length,
130
130
  );
131
131
  });
132
132
  });
@@ -136,14 +136,20 @@ describe('workflow-blocker items are filtered by the done overlay like every oth
136
136
  const overlay: ConsoleOverlay = {
137
137
  PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
138
138
  };
139
- expect(countPendingItems([item(1), item(2)], overlay)).toBe(1);
139
+ expect(
140
+ countPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
141
+ ).toBe(1);
140
142
  });
141
143
 
142
144
  it('removes a processed workflow-blocker item from the list', () => {
143
145
  const overlay: ConsoleOverlay = {
144
146
  PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
145
147
  };
146
- const result = filterPendingItems([item(1), item(2)], overlay);
148
+ const result = filterPendingItems(
149
+ [item(1), item(2)],
150
+ overlay,
151
+ 'workflow-blocker',
152
+ );
147
153
  expect(result.map((entry) => entry.number)).toEqual([2]);
148
154
  });
149
155
 
@@ -152,8 +158,12 @@ describe('workflow-blocker items are filtered by the done overlay like every oth
152
158
  PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
153
159
  PVTI_2: { ts: 100, mode: 'workflow-blocker', done: true },
154
160
  };
155
- expect(countPendingItems([item(1), item(2)], overlay)).toBe(0);
156
- expect(filterPendingItems([item(1), item(2)], overlay)).toEqual([]);
161
+ expect(
162
+ countPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
163
+ ).toBe(0);
164
+ expect(
165
+ filterPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
166
+ ).toEqual([]);
157
167
  });
158
168
  });
159
169
 
@@ -171,8 +181,10 @@ describe('overlayEntriesActedSinceSnapshot', () => {
171
181
  '2026-08-04T00:26:06Z',
172
182
  );
173
183
  expect(acted).toEqual({});
174
- expect(countPendingItems([item(1)], acted)).toBe(1);
175
- expect(filterPendingItems([item(1)], acted)).toEqual([item(1)]);
184
+ expect(countPendingItems([item(1)], acted, 'todo-by-human')).toBe(1);
185
+ expect(filterPendingItems([item(1)], acted, 'todo-by-human')).toEqual([
186
+ item(1),
187
+ ]);
176
188
  });
177
189
 
178
190
  it('keeps an entry written after the snapshot was generated so the item stays hidden', () => {
@@ -187,7 +199,7 @@ describe('overlayEntriesActedSinceSnapshot', () => {
187
199
  overlay,
188
200
  '2026-08-04T00:26:06Z',
189
201
  );
190
- expect(countPendingItems([item(1)], acted)).toBe(0);
202
+ expect(countPendingItems([item(1)], acted, 'todo-by-human')).toBe(0);
191
203
  });
192
204
 
193
205
  it('keeps an entry written at the exact snapshot generation time', () => {
@@ -16,19 +16,28 @@ export const isOverlayEntryActed = (
16
16
  entry: ConsoleOverlayEntry | undefined,
17
17
  ): boolean => entry !== undefined && entry.done === true;
18
18
 
19
+ const isActedInTab = (
20
+ entry: ConsoleOverlayEntry | undefined,
21
+ currentTab: ConsoleTabName,
22
+ ): boolean =>
23
+ entry !== undefined && entry.done === true && entry.mode === currentTab;
24
+
19
25
  export const countPendingItems = (
20
26
  items: ConsoleListItem[],
21
27
  overlay: ConsoleOverlay,
28
+ currentTab: ConsoleTabName,
22
29
  ): number =>
23
- items.filter((item) => !isOverlayEntryActed(overlay[overlayKeyForItem(item)]))
24
- .length;
30
+ items.filter(
31
+ (item) => !isActedInTab(overlay[overlayKeyForItem(item)], currentTab),
32
+ ).length;
25
33
 
26
34
  export const filterPendingItems = (
27
35
  items: ConsoleListItem[],
28
36
  overlay: ConsoleOverlay,
37
+ currentTab: ConsoleTabName,
29
38
  ): ConsoleListItem[] =>
30
39
  items.filter(
31
- (item) => !isOverlayEntryActed(overlay[overlayKeyForItem(item)]),
40
+ (item) => !isActedInTab(overlay[overlayKeyForItem(item)], currentTab),
32
41
  );
33
42
 
34
43
  export const overlayEntriesActedSinceSnapshot = (
@@ -107,6 +107,7 @@ export const ConsolePage = () => {
107
107
  overlayState.overlay,
108
108
  snapshot.generatedAt,
109
109
  ),
110
+ tab.name,
110
111
  );
111
112
  }
112
113
  }
@@ -151,8 +152,9 @@ export const ConsolePage = () => {
151
152
  overlayState.overlay,
152
153
  activeSnapshot.generatedAt,
153
154
  ),
155
+ activeTab,
154
156
  );
155
- }, [activeSnapshot, overlayState.overlay]);
157
+ }, [activeSnapshot, overlayState.overlay, activeTab]);
156
158
 
157
159
  const orderedPendingKeys = useMemo(
158
160
  () => pendingItems.map((item) => overlayKeyForItem(item)),