@gitkraken/core-gitlens 0.5.104 → 0.5.106

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 (80) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/git/models/pullRequest.d.ts +47 -0
  3. package/dist/git/models/pullRequest.d.ts.map +1 -1
  4. package/dist/git/models/pullRequest.js +8 -0
  5. package/dist/git/models/pullRequest.js.map +1 -1
  6. package/dist/plus/git-github/api/github.d.ts +33 -1
  7. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  8. package/dist/plus/git-github/api/github.js +176 -0
  9. package/dist/plus/git-github/api/github.js.map +1 -1
  10. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +16 -0
  11. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -0
  12. package/dist/plus/git-github/api/pullRequestSearchQuery.js +64 -0
  13. package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -0
  14. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +1 -0
  15. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -1
  16. package/dist/plus/integrations/authentication/configuredIntegrationService.js +4 -1
  17. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  18. package/dist/plus/integrations/index.d.ts +1 -0
  19. package/dist/plus/integrations/index.d.ts.map +1 -1
  20. package/dist/plus/integrations/integrationService.d.ts +16 -2
  21. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  22. package/dist/plus/integrations/integrationService.js +34 -23
  23. package/dist/plus/integrations/integrationService.js.map +1 -1
  24. package/dist/plus/integrations/manager.d.ts +46 -4
  25. package/dist/plus/integrations/manager.d.ts.map +1 -1
  26. package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -2
  27. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  28. package/dist/plus/integrations/models/gitHostIntegration.js +21 -0
  29. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  30. package/dist/plus/integrations/models/integration.d.ts +2 -1
  31. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  32. package/dist/plus/integrations/models/integration.js.map +1 -1
  33. package/dist/plus/integrations/models/pullRequestReads.d.ts +18 -0
  34. package/dist/plus/integrations/models/pullRequestReads.d.ts.map +1 -0
  35. package/dist/plus/integrations/models/pullRequestReads.js +2 -0
  36. package/dist/plus/integrations/models/pullRequestReads.js.map +1 -0
  37. package/dist/plus/integrations/providerFilters.d.ts +7 -6
  38. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  39. package/dist/plus/integrations/providerFilters.js +1 -7
  40. package/dist/plus/integrations/providerFilters.js.map +1 -1
  41. package/dist/plus/integrations/providers/github.d.ts +14 -2
  42. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  43. package/dist/plus/integrations/providers/github.js +15 -0
  44. package/dist/plus/integrations/providers/github.js.map +1 -1
  45. package/dist/plus/integrations/providers/models.d.ts +12 -2
  46. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  47. package/dist/plus/integrations/providers/models.js +18 -0
  48. package/dist/plus/integrations/providers/models.js.map +1 -1
  49. package/dist/plus/integrations/reads/context.d.ts +1 -1
  50. package/dist/plus/integrations/reads/filters.d.ts +34 -1
  51. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  52. package/dist/plus/integrations/reads/filters.js +73 -0
  53. package/dist/plus/integrations/reads/filters.js.map +1 -1
  54. package/dist/plus/integrations/reads/searchPullRequests.d.ts +26 -0
  55. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -0
  56. package/dist/plus/integrations/reads/searchPullRequests.js +108 -0
  57. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -0
  58. package/dist/plus/integrations/reads/warnings.d.ts +6 -2
  59. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  60. package/dist/plus/integrations/reads/warnings.js +40 -0
  61. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  62. package/docs/integrations.md +53 -4
  63. package/package.json +1 -1
  64. package/src/git/models/pullRequest.ts +50 -0
  65. package/src/plus/git-github/api/github.ts +254 -1
  66. package/src/plus/git-github/api/pullRequestSearchQuery.ts +80 -0
  67. package/src/plus/integrations/authentication/configuredIntegrationService.ts +6 -1
  68. package/src/plus/integrations/index.ts +2 -0
  69. package/src/plus/integrations/integrationService.ts +108 -70
  70. package/src/plus/integrations/manager.ts +55 -4
  71. package/src/plus/integrations/models/gitHostIntegration.ts +56 -2
  72. package/src/plus/integrations/models/integration.ts +2 -0
  73. package/src/plus/integrations/models/pullRequestReads.ts +18 -0
  74. package/src/plus/integrations/providerFilters.ts +8 -6
  75. package/src/plus/integrations/providers/github.ts +37 -1
  76. package/src/plus/integrations/providers/models.ts +37 -2
  77. package/src/plus/integrations/reads/context.ts +1 -1
  78. package/src/plus/integrations/reads/filters.ts +105 -0
  79. package/src/plus/integrations/reads/searchPullRequests.ts +210 -0
  80. package/src/plus/integrations/reads/warnings.ts +73 -3
@@ -5,6 +5,8 @@ import type {
5
5
  IssueSearchCriteria,
6
6
  IssueSearchRelationship,
7
7
  PullRequestFilter,
8
+ PullRequestSearchCapabilities,
9
+ PullRequestSearchCriteria,
8
10
  } from '../providerFilters.js';
9
11
  import type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';
10
12
  import { providersMetadata } from '../providers/models.js';
@@ -65,6 +67,88 @@ export function resolveAccountWidePullRequestFilters(
65
67
  return { filters: [...new Set(filters)], unsupported: false };
66
68
  }
67
69
 
70
+ /** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */
71
+ export type PullRequestSearchCriteriaRejection =
72
+ /** The provider has no filtered pull-request search. */
73
+ | { reason: 'unsupported-search' }
74
+ /** The provider has one, but cannot express every requested criterion server-side. */
75
+ | { reason: 'unsupported-criteria'; criteria: string[] };
76
+
77
+ /**
78
+ * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would
79
+ * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the
80
+ * request before any upstream call instead.
81
+ */
82
+ export function resolvePullRequestSearchCriteria(
83
+ id: IntegrationIds,
84
+ criteria: PullRequestSearchCriteria | undefined,
85
+ ): { rejection?: PullRequestSearchCriteriaRejection } {
86
+ const supported = providersMetadata[id]?.supportedPullRequestSearch;
87
+ if (supported == null || supported.relationships.length === 0) {
88
+ return { rejection: { reason: 'unsupported-search' } };
89
+ }
90
+ if (criteria == null) return {};
91
+
92
+ const unsupported: string[] = [];
93
+ for (const relationship of criteria.relationships ?? []) {
94
+ if (!supported.relationships.includes(relationship)) {
95
+ unsupported.push(`relationships:${relationship}`);
96
+ }
97
+ }
98
+ for (const state of criteria.states ?? []) {
99
+ if (!supported.states.includes(state)) {
100
+ unsupported.push(`states:${state}`);
101
+ }
102
+ }
103
+ if (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {
104
+ unsupported.push('text');
105
+ }
106
+ if (criteria.includeArchived === true && !supported.includeArchived) {
107
+ unsupported.push('includeArchived');
108
+ }
109
+
110
+ return unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};
111
+ }
112
+
113
+ /** Why a filtered pull-request search's repository/organization boundary was refused. */
114
+ export type PullRequestSearchScopeRejection =
115
+ | 'unscoped'
116
+ | 'repo-ids'
117
+ | 'unsupported-repository-scope'
118
+ | 'unsupported-organization-scope';
119
+
120
+ /**
121
+ * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe
122
+ * account-wide boundary; without one, a repository or organization scope is mandatory.
123
+ */
124
+ export function resolvePullRequestSearchScope(
125
+ id: IntegrationIds,
126
+ repos: ProviderReposInput | undefined,
127
+ org: string | undefined,
128
+ criteria: PullRequestSearchCriteria | undefined,
129
+ ): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {
130
+ const supported = providersMetadata[id]?.supportedPullRequestSearch;
131
+ let resolvedRepos: ProviderRepoInput[] | undefined;
132
+
133
+ if (repos?.length) {
134
+ if (repos.some(r => typeof r === 'string' || typeof r === 'number')) return { rejection: 'repo-ids' };
135
+ if (supported?.repositoryScope !== true) return { rejection: 'unsupported-repository-scope' };
136
+
137
+ resolvedRepos = repos as ProviderRepoInput[];
138
+ }
139
+
140
+ const hasOrganizationScope = org != null && org.length > 0;
141
+ if (hasOrganizationScope && supported?.organizationScope !== true) {
142
+ return { rejection: 'unsupported-organization-scope' };
143
+ }
144
+
145
+ if (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {
146
+ return { repos: resolvedRepos };
147
+ }
148
+
149
+ return { rejection: 'unscoped' };
150
+ }
151
+
68
152
  /**
69
153
  * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express
70
154
  * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —
@@ -182,6 +266,16 @@ const unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {
182
266
  states: false,
183
267
  };
184
268
 
269
+ /** What a provider with no filtered pull-request search reports. */
270
+ const unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {
271
+ relationships: [],
272
+ states: [],
273
+ text: false,
274
+ includeArchived: false,
275
+ repositoryScope: false,
276
+ organizationScope: false,
277
+ };
278
+
185
279
  /** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */
186
280
  export type IssueSearchScopeRejection =
187
281
  /** No repositories, no org, and no user-relative relationship: a search of the whole host. */
@@ -248,6 +342,9 @@ export function resolveIssueSearchScope(
248
342
  * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has
249
343
  * no filtered issue search — hide the surface, not just its chips.
250
344
  *
345
+ * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary
346
+ * plus repository/organization scope support; an empty relationship list means the search itself is absent.
347
+ *
251
348
  * Note this is a CAPABILITY table — "what the provider can express" — not a recommendation. A consumer
252
349
  * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an
253
350
  * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a
@@ -256,15 +353,23 @@ export function resolveIssueSearchScope(
256
353
  export function getSupportedFilters(providerId: IntegrationIds): {
257
354
  pullRequests: PullRequestFilter[];
258
355
  pullRequestsAccountWide: PullRequestFilter[];
356
+ pullRequestSearch: PullRequestSearchCapabilities;
259
357
  issues: IssueFilter[];
260
358
  issuesAccountWide: IssueFilter[];
261
359
  issueSearch: IssueSearchCapabilities;
262
360
  } {
263
361
  const metadata = providersMetadata[providerId];
264
362
  const issueSearch = metadata?.supportedIssueSearch;
363
+ const pullRequestSearch = metadata?.supportedPullRequestSearch;
265
364
  return {
266
365
  pullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],
267
366
  pullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],
367
+ pullRequestSearch: {
368
+ ...unsupportedPullRequestSearchCapabilities,
369
+ ...pullRequestSearch,
370
+ relationships: [...(pullRequestSearch?.relationships ?? [])],
371
+ states: [...(pullRequestSearch?.states ?? [])],
372
+ },
268
373
  issues: [...(metadata?.supportedIssueFilters ?? [])],
269
374
  issuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],
270
375
  // Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose
@@ -0,0 +1,210 @@
1
+ import type { PullRequestSearchCriteria, PullRequestShape } from '../../../git/models/pullRequest.js';
2
+ import type { IntegrationIds } from '../constants.js';
3
+ import type { ProviderReposInput } from '../providers/models.js';
4
+ import type { ProviderPagedResult, ProviderWarning } from '../results.js';
5
+ import {
6
+ isGitHostIntegration,
7
+ isIssuesHostIntegrationId,
8
+ warnOnMissingSessionForDomain,
9
+ } from '../utils/integration.utils.js';
10
+ import type { ProviderReadContext } from './context.js';
11
+ import { runCaptured } from './drains.js';
12
+ import { resolvePullRequestSearchCriteria, resolvePullRequestSearchScope } from './filters.js';
13
+ import {
14
+ drainFlatPagesToRequestedPage,
15
+ refusedPage,
16
+ resolveContinuation,
17
+ resolveCurrentPage,
18
+ usableCursor,
19
+ } from './paging.js';
20
+ import {
21
+ gitHostOnlySurfaceWarning,
22
+ otherWarning,
23
+ pullRequestSearchCapResultWarning,
24
+ truncationWarning,
25
+ unsupportedPullRequestSearchCriteriaWarning,
26
+ } from './warnings.js';
27
+
28
+ /**
29
+ * Pull requests matching structured criteria over a repository/organization or current-user relationship scope.
30
+ *
31
+ * The read is deliberately separate from `listPullRequestsPage`: the list has provider-specific "my PRs"
32
+ * semantics and no text channel, while this method promises a sanitized query, most-recently-updated-first
33
+ * ordering, one upstream request per cursor-threaded page, and a quantified omission at the provider's result
34
+ * ceiling.
35
+ */
36
+ export async function searchPullRequestsPage(
37
+ ctx: ProviderReadContext,
38
+ options: {
39
+ providerId: IntegrationIds;
40
+ repos?: ProviderReposInput;
41
+ org?: string;
42
+ criteria?: PullRequestSearchCriteria;
43
+ page?: number;
44
+ cursor?: string;
45
+ itemsPerPage?: number;
46
+ forceSync?: boolean;
47
+ connectionId?: string;
48
+ domain?: string;
49
+ },
50
+ ): Promise<ProviderPagedResult<PullRequestShape>> {
51
+ const page = Math.max(1, Math.trunc(options.page ?? 1));
52
+ const refused = (warning: ProviderWarning) => refusedPage<PullRequestShape>(1, [warning], true);
53
+
54
+ if (isIssuesHostIntegrationId(options.providerId)) {
55
+ return refused(
56
+ gitHostOnlySurfaceWarning(
57
+ options.providerId,
58
+ undefined,
59
+ options.connectionId,
60
+ 'Filtered pull request search',
61
+ ),
62
+ );
63
+ }
64
+
65
+ const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
66
+ if (integration == null) {
67
+ const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);
68
+ return refusedPage(1, early.warnings, early.fetchFailed);
69
+ }
70
+ if (!isGitHostIntegration(integration)) {
71
+ return refused(
72
+ gitHostOnlySurfaceWarning(
73
+ options.providerId,
74
+ undefined,
75
+ options.connectionId,
76
+ 'Filtered pull request search',
77
+ ),
78
+ );
79
+ }
80
+
81
+ await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
82
+
83
+ const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
84
+ const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
85
+
86
+ const resolved = resolvePullRequestSearchCriteria(options.providerId, options.criteria);
87
+ if (resolved.rejection != null) {
88
+ return refused(
89
+ unsupportedPullRequestSearchCriteriaWarning(
90
+ options.providerId,
91
+ domain,
92
+ options.connectionId,
93
+ resolved.rejection,
94
+ ),
95
+ );
96
+ }
97
+
98
+ const scope = resolvePullRequestSearchScope(options.providerId, options.repos, options.org, options.criteria);
99
+ switch (scope.rejection) {
100
+ case 'unscoped':
101
+ return refused(
102
+ otherWarning(
103
+ options.providerId,
104
+ domain,
105
+ options.connectionId,
106
+ 'A filtered pull request search must be scoped: pass `repos`, `org`, or at least one current-user relationship.',
107
+ ),
108
+ );
109
+ case 'repo-ids':
110
+ return refused(
111
+ otherWarning(
112
+ options.providerId,
113
+ domain,
114
+ options.connectionId,
115
+ 'A filtered pull request search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',
116
+ ),
117
+ );
118
+ case 'unsupported-repository-scope':
119
+ case 'unsupported-organization-scope':
120
+ return refused(
121
+ unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
122
+ reason: 'unsupported-criteria',
123
+ criteria: [
124
+ scope.rejection === 'unsupported-repository-scope' ? 'repositoryScope' : 'organizationScope',
125
+ ],
126
+ }),
127
+ );
128
+ }
129
+
130
+ const readPage = (cursor: string | undefined) =>
131
+ runCaptured(
132
+ options.providerId,
133
+ domain,
134
+ options.connectionId,
135
+ () =>
136
+ integration.searchPullRequestsPageResult(
137
+ {
138
+ repos: scope.repos,
139
+ org: options.org,
140
+ criteria: options.criteria,
141
+ cursor: cursor,
142
+ pageSize: options.itemsPerPage,
143
+ },
144
+ undefined,
145
+ options.connectionId,
146
+ ),
147
+ { warnOnMissingSession: warnOnMissingSession },
148
+ );
149
+
150
+ const first = await readPage(options.cursor);
151
+ const warnings = first.warning != null ? [first.warning] : [];
152
+ let totalCount = first.value?.totalCount;
153
+ const drained = await drainFlatPagesToRequestedPage(first, {
154
+ requestedPage: page,
155
+ suppliedCursor: options.cursor,
156
+ warnings: warnings,
157
+ readPage: readPage,
158
+ fold: p => {
159
+ if (p.totalCount != null) {
160
+ totalCount = Math.max(totalCount ?? 0, p.totalCount);
161
+ }
162
+ },
163
+ });
164
+ const { value, currentPage, requestedPageMissing } = drained;
165
+
166
+ if (value == null && warnings.length === 0) {
167
+ return refused(
168
+ unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
169
+ reason: 'unsupported-search',
170
+ }),
171
+ );
172
+ }
173
+
174
+ const items = requestedPageMissing ? [] : (value?.values ?? []);
175
+ const continuation = resolveContinuation(
176
+ {
177
+ hasMore: requestedPageMissing ? false : (value?.hasMore ?? false),
178
+ cursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),
179
+ truncated: drained.truncated,
180
+ },
181
+ undefined,
182
+ );
183
+ const truncated = continuation.truncated;
184
+ if (truncated && warnings.length === 0) {
185
+ warnings.push(
186
+ pullRequestSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount) ??
187
+ truncationWarning(options.providerId, domain, options.connectionId, 'Pull request search', 'exhausted'),
188
+ );
189
+ }
190
+
191
+ return {
192
+ items: items,
193
+ warnings: warnings,
194
+ page: {
195
+ currentPage: requestedPageMissing
196
+ ? page
197
+ : resolveCurrentPage({
198
+ providerPage: currentPage,
199
+ requestedPage: page,
200
+ suppliedCursor: options.cursor,
201
+ pageAdvanceable: false,
202
+ }),
203
+ itemsPerPage: items.length,
204
+ truncated: truncated || undefined,
205
+ },
206
+ hasMore: continuation.hasMore,
207
+ cursor: continuation.cursor,
208
+ fetchFailed: drained.fetchFailed || undefined,
209
+ };
210
+ }
@@ -1,8 +1,13 @@
1
1
  import type { IntegrationIds } from '../constants.js';
2
- import type { IssueFilter, IssueSearchCapabilities, PullRequestFilter } from '../providerFilters.js';
2
+ import type {
3
+ IssueFilter,
4
+ IssueSearchCapabilities,
5
+ PullRequestFilter,
6
+ PullRequestSearchCapabilities,
7
+ } from '../providerFilters.js';
3
8
  import { providersMetadata } from '../providers/models.js';
4
9
  import type { ProviderWarning } from '../results.js';
5
- import type { IssueSearchCriteriaRejection } from './filters.js';
10
+ import type { IssueSearchCriteriaRejection, PullRequestSearchCriteriaRejection } from './filters.js';
6
11
 
7
12
  /**
8
13
  * The warnings the provider facade raises on its OWN terms, as opposed to the ones derived from a caught
@@ -154,7 +159,8 @@ type TruncatedReadKind =
154
159
  | 'Organization'
155
160
  | 'Project'
156
161
  | 'Account-wide issue search'
157
- | 'Issue search';
162
+ | 'Issue search'
163
+ | 'Pull request search';
158
164
 
159
165
  /** {@link incompleteReadWarning} for a paged or drained read, phrased per surface. */
160
166
  export function truncationWarning(
@@ -246,6 +252,44 @@ export function unsupportedFiltersWarning(
246
252
  );
247
253
  }
248
254
 
255
+ /** Warning for a filtered pull-request search the provider cannot run as requested. */
256
+ export function unsupportedPullRequestSearchCriteriaWarning(
257
+ id: IntegrationIds,
258
+ domain: string | undefined,
259
+ connectionId: string | undefined,
260
+ rejection: PullRequestSearchCriteriaRejection,
261
+ ): ProviderWarning {
262
+ let message: string;
263
+ switch (rejection.reason) {
264
+ case 'unsupported-search':
265
+ message = `Filtered pull request search is not supported by '${id}'; use \`listPullRequestsPage\` for its own pull request reads.`;
266
+ break;
267
+ case 'unsupported-criteria': {
268
+ const supported = providersMetadata[id]?.supportedPullRequestSearch;
269
+ message = unsupportedNarrowingMessage(
270
+ id,
271
+ 'pull request search criteria',
272
+ rejection.criteria.join(', '),
273
+ supported != null ? describePullRequestSearchCapabilities(supported) : '',
274
+ );
275
+ break;
276
+ }
277
+ }
278
+
279
+ return otherWarning(id, domain, connectionId, message);
280
+ }
281
+
282
+ function describePullRequestSearchCapabilities(capabilities: PullRequestSearchCapabilities): string {
283
+ return [
284
+ ...(capabilities.relationships.length ? [`relationships:${capabilities.relationships.join('|')}`] : []),
285
+ ...(capabilities.states.length ? [`states:${capabilities.states.join('|')}`] : []),
286
+ ...(capabilities.text ? ['text'] : []),
287
+ ...(capabilities.includeArchived ? ['includeArchived'] : []),
288
+ ...(capabilities.repositoryScope ? ['repository scope'] : []),
289
+ ...(capabilities.organizationScope ? ['organization scope'] : []),
290
+ ].join(', ');
291
+ }
292
+
249
293
  /**
250
294
  * Warning for a filtered issue search whose criteria the provider can't serve — either it has no such search at
251
295
  * all, or it can't express what was asked for, or the set contradicts itself.
@@ -343,6 +387,32 @@ export function issueSearchCapResultWarning(
343
387
  };
344
388
  }
345
389
 
390
+ /** Quantified omission for a pull-request search that exceeded the provider's reachable result window. */
391
+ export function pullRequestSearchCapResultWarning(
392
+ id: IntegrationIds,
393
+ domain: string | undefined,
394
+ connectionId: string | undefined,
395
+ totalCount: number | undefined,
396
+ ): ProviderWarning | undefined {
397
+ const limit = providersMetadata[id]?.pullRequestSearchResultLimit;
398
+ if (limit == null || totalCount == null || totalCount <= limit) return undefined;
399
+
400
+ return {
401
+ ...otherWarning(
402
+ id,
403
+ domain,
404
+ connectionId,
405
+ `Pull request search matched ${totalCount} results, but '${id}' serves at most ${limit}; narrow the search to read the rest.`,
406
+ ),
407
+ omission: {
408
+ kind: 'provider-limit',
409
+ recovery: 'none',
410
+ limit: limit,
411
+ totalCount: totalCount,
412
+ },
413
+ };
414
+ }
415
+
346
416
  /** Warning for a git host that doesn't expose issues on this surface (e.g. Bitbucket, deprecated in favor of Jira). */
347
417
  export function issuesUnsupportedWarning(
348
418
  id: IntegrationIds,